6.x bootstrap.inc ip_address()
7.x bootstrap.inc ip_address()

If Drupal is behind a reverse proxy, we use the X-Forwarded-For header instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of the proxy server, and not the client's.

Return value

IP address of client machine, adjusted for reverse proxy.

7 calls to ip_address()
drupal_anonymous_user in includes/bootstrap.inc
Generates a default anonymous $user object.
flood_is_allowed in includes/common.inc
Check if the current visitor (hostname/IP) is allowed to proceed with the specified event.
flood_register_event in includes/common.inc
Register an event for the current visitor (hostname/IP) to the flood control mechanism.
sess_write in includes/session.inc
Writes an entire session to the database (internal use only).
statistics_exit in modules/statistics/statistics.module
Implementation of hook_exit().

... See full list

File

includes/bootstrap.inc, line 1361
Functions that need to be loaded on every Drupal request.

Code

function ip_address() {
  static $ip_address = NULL;
  if (!isset($ip_address)) {
    $ip_address = $_SERVER['REMOTE_ADDR'];
    if (variable_get('reverse_proxy', 0) && array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {

      // If an array of known reverse proxy IPs is provided, then trust
      // the XFF header if request really comes from one of them.
      $reverse_proxy_addresses = variable_get('reverse_proxy_addresses', array());
      if (!empty($reverse_proxy_addresses) && in_array($ip_address, $reverse_proxy_addresses, TRUE)) {

        // If there are several arguments, we need to check the most
        // recently added one, i.e. the last one.
        $ip_address_parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        $ip_address = array_pop($ip_address_parts);
      }
    }
  }
  return $ip_address;
}

Comments

robpeek’s picture

Our proxy redirects a request to itself on a different port, for load balancing benefits, hence the last added ip is 127.0.0.1

The code below provides a answer to this problem, although you have to be careful when updating your install.

function ip_address() {
  static $ip_address = NULL;

  if (!isset($ip_address)) {
    $ip_address = $_SERVER['REMOTE_ADDR'];
    if (variable_get('reverse_proxy', 0) && array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
      // If an array of known reverse proxy IPs is provided, then trust
      // the XFF header if request really comes from one of them.
      $reverse_proxy_addresses = variable_get('reverse_proxy_addresses', array());
      if (!empty($reverse_proxy_addresses) && in_array($ip_address, $reverse_proxy_addresses, TRUE)) {
        // If there are several arguments, we need to check the most
        // recently added one, i.e. the last one.
        $ip_list = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
        $ip_address = array_pop($ip_list);
	while(in_array($ip_address, $reverse_proxy_addresses)){
  			$ip_address = trim(array_pop($ip_list));
  	}
      }
    }
  }

  return $ip_address;
}
mikeytown2’s picture

Make sure to link to the issue
http://drupal.org/node/705718

EvanDonovan’s picture

D6 issue currently awaiting testing is: http://drupal.org/node/621748.

chadhester’s picture

On an Acquia server where I'm trying to configure settings.php properly, so far we have the $conf['reverse_proxy_addresses'] array populating with the 10.*.*.* proxy IP address values. For reference, this is the code snippet that Acquia suggested that I use at the end of the settings.php file, just below their require() call:

<?php
$conf['reverse_proxy'] = TRUE;
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
  $ips = array_map('trim', $ips);
  // Add REMOTE_ADDR to the X-Forwarded-For list (the ip_address function will also do this) 
  //  in case it's a 10. internal AWS address; if it is we should add it to the list of
  //  reverse proxy addresses so that ip_address will ignore it.
  $ips[] = $_SERVER['REMOTE_ADDR'];
  // Work backwards through the list of IPs, adding 10. addresses to the proxy list
  //  but stop at the first non-10. address we find. 
  $ips = array_reverse($ips);
  foreach ($ips as $ip) {
    if (strpos($ip, '10.') === 0) {
      if (!in_array($ip, $conf['reverse_proxy_addresses'])) {
        $conf['reverse_proxy_addresses'][] = $ip;
      }
    }
    else {
      // we hit the first non-10. address, so stop
      break;
    }
  }
}

?>

According to the spec, the **first** IP address listed should be that of the client.
See:
http://en.wikipedia.org/wiki/X-Forwarded-For#Format
http://www.openinfo.co.uk/apache/index.html#descr

X-Forwarded-For: client, proxy1, proxy2

When I check the value of $_SERVER['HTTP_X_FORWARDED_FOR'], the first value is indeed my IP address, followed by one entry for one of the Acquia IPs. So am I just tired and misunderstanding things... why array_pop() to get the last value? Shouldn't this be pulling the first value?

chadhester’s picture

Okay, so to answer my own question (always a good time): The header can be modified by the client, so you cannot always trust the left-most value. Also, the code that Acquia originally provided was specifically for D7, which does some additional work that the ip_address() function in D6 doesn't do. Here's what Acquia had to say, followed by the code that they recommend we use in settings.php:

You're correct that the workaround we provided was written for D7 and that it doesn't work correctly for D6. Thanks for bringing this to our attention.

The problem is that the ip_address function in D6 doesn't perform one of the steps which was added in D7:

https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/ip_a...

https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/ip_a...

The D7 version uses the reverse_proxy_addresses array to remove IPs of "trusted proxies" from the list in the X-Forwarded-For header and then takes the IP address furthest to the right in the list that remains.

The reason it does this instead of taking the IP address furthest to the left is that it's trivial to spoof extra IPs to the left in the XFF header (you can do it just by supplying an XFF header with the request that you send, e.g. using a browser add-on).

So, here's an amended snippet which checks whether the site is D6, and if so performs the additional step of removing the list of "trusted proxies" from the XFF header. This means that the ip_address function should work as it does in D7 and return the right-most "untrusted" address in the XFF header, which should usually be the client IP.

- Drew Webber, Acquia Client Advisory Team

<?php
// Required to get the correct client IP address on Acquia's ELB prod servers:
$conf['reverse_proxy'] = TRUE;
if (!is_array($conf['reverse_proxy_addresses'])) {
  $conf['reverse_proxy_addresses'] = array();
}
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  $x_ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
  $x_ips = array_map('trim', $x_ips);
  // Add REMOTE_ADDR to the X-Forwarded-For list (the ip_address function will also do this) 
  //  in case it's a 10. internal AWS address; if it is we should add it to the list of
  //  reverse proxy addresses so that ip_address will ignore it.
  $x_ips[] = $_SERVER['REMOTE_ADDR'];
  // Work backwards through the list of IPs, adding 10. addresses to the proxy list
  //  but stop at the first non-10. address we find. 
  $x_ips_rev = array_reverse($x_ips);
  foreach ($x_ips_rev as $ip) {
    if (strpos($ip, '10.') === 0) {
      if (!in_array($ip, $conf['reverse_proxy_addresses'])) {
        $conf['reverse_proxy_addresses'][] = $ip;
      }
    }
    else {
      // we hit the first non-10. address, so stop
      break;
    }
  }
  if ((defined('DRUPAL_CORE_COMPATIBILITY') && DRUPAL_CORE_COMPATIBILITY == '6.x')
      || function_exists('drupal_init_language')) {
    // in D6, the ip_address function doesn't perform the 'Eliminate all trusted IPs' step
    //  which strips the reverse_proxy_addresses from those in the XFF header, so we have to do that here
    $untrusted = array_diff($x_ips, $conf['reverse_proxy_addresses']);
    $_SERVER['HTTP_X_FORWARDED_FOR'] = implode(', ', $untrusted);
  }
}

?>

This alteration worked without a hitch, this time.

Many thanks to Drew Webber for revising this code, Charissa Cotrill for her initial guidance, and the rest of the Acquia team for this educational experience. :)

s_afshar85’s picture

Thanks for the great post.