X-Forwarded-For

Http header X-Forwarded-For can be used to get the IP address of the REAL client, especially in a network with proxies and load balancers.

The X-Forwarded-For (XFF) header is a de-facto standard header for identifying the originating IP address of a client connecting to a web server through an HTTP proxy or a load balancer. When traffic is intercepted between clients and servers, server access logs contain the IP address of the proxy or load balancer only. To see the original IP address of the client, the X-Forwarded-For request header is used.

The syntax is,

X-Forwarded-For: <client>, <proxy1>, <proxy2>
X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178

When a Http request flows through a proxy, the proxy appends its IP address to X-Forwarded-For header (if it respects this header).

Forwarded

However, since X- headers are not recommended anymore,

Custom proprietary headers can be added using the ‘X-‘ prefix, but this convention was deprecated in June 2012.

a standardized and enhanced header, Forwarded, is introduced.

# the original request is from 192.0.2.60, and passed through proxy 203.0.113.43
Forwarded: for=192.0.2.60; proto=http; by=203.0.113.43

# client can also append some obfuscated identifier like "secret" here, server can 
# then use it validate the integrity of a client.
Forwarded: for=23.45.67.89;secret=egah2CGj55fSJFs, for=10.1.2.3
X-Real-IP

Another somehow relevant header is X-Real-IP, which contains a single IP. You may find it, for example, somewhere in Nginx docs (ngx_http_proxy_module doc, ngx_http_realip_module doc).

location / {
    proxy_pass       http://localhost:8000;
    proxy_set_header Host      $host;
    proxy_set_header X-Real-IP $remote_addr;
}
Which Header to Use (and Trust)

So which header to use, if you want to client IP address in server site?

It denpends on how much you know the network between the client and the server, and how much you trust these headers.

If you know there is no component sitting between your clients and your server, using request.getRemoteAddress() is fair and enough.

If besides the nginx you deploy (with below configuration), there is no others sitting in between,

location / {
    proxy_pass       http://localhost:8000;
    proxy_set_header X-Real-IP $remote_addr;
}

then using request.getHeader("X-Real-IP") will fetch client’s IP correctly. Also X-Real-IP here is trustable, since it’s set by trusted code (your own ngnix configuration).

If you’re not sure how many proxies/load balancers there, then X-Forwarded-For or Forwarded can be used to get client IP, if present. However, since any proxy can modify/add these headers freely, there is no guarantee the IP is of a real client. To let server authenticate client IP, clients can add some secret tokens, which the server understands, in Forwarded header, as mentioned before.

Nginx

Nginx supports these headers.

A use case of X-Real-IP can be seen above.

Nginx also provides a module, ngx_http_realip_module, which understands X-Forwarded-For and can help extract client ip from this header and save it to $realip_remote_addr for late use.

If Nginx acts as a proxy, it also has a responsibility to construct X-Forwarded-For header correctly, so that the server(s) behind it can rely on this header if needed. Nginx’s ngx_http_proxy_module provides an embedded variable $proxy_add_x_forwarded_for to set the header easily.

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

The above line will append IP address of whom sending requests to Nginx (maybe a real client, maybe a proxy) to X-Forwarded-For header. The proxied server will then see,

X-Forwarded-For: <client-ip>, <proxy-0-before-your-nginx>, <proxy-1-before-your-nginx>

Nginx supports Forwarded (though no equivalence like $proxy_add_x_forwarded_for), as demonstrated in this doc.

map $remote_addr $proxy_forwarded_elem {
    # IPv4 addresses can be sent as-is
    ~^[0-9.]+$          "for=$remote_addr";

    # IPv6 addresses need to be bracketed and quoted
    ~^[0-9A-Fa-f:.]+$   "for=\"[$remote_addr]\"";

    # Unix domain socket names cannot be represented in RFC 7239 syntax
    default             "for=unknown";
}

map $http_forwarded $proxy_add_forwarded {
    # If the incoming Forwarded header is syntactically valid, append to it
    "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";

    # Otherwise, replace it
    default "$proxy_forwarded_elem";
}

proxy_set_header Forwarded $proxy_add_forwarded;

Your Nginx can feed all these headers to proxied server simultaneously, let the server decide which one to use.

proxy_set_header Forwarded $proxy_add_forwarded;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Server Code

Server can get client IP based on presence and trustability of these headers. For example,

String getClientIp() {
	if (!header_A.isBlank()) { // the header you trust best
	  return extractIpFrom(header_A);
	} else if (!header_B.isBlank()) {
	  return extractIpFrom(header_B);
	} // else if ...
	
	// if no these headers, ...
	return request.getRemoteAddr();
}

This site registers some Bitbucket webhooks. To whitelist Bitbucket IP addresses, it simply extract IP address from requests, put it into X-Real-IP, and pass it to the proxied Rails app.

location / {
    proxy_pass       http://rails-app;
    proxy_set_header X-Real-IP $remote_addr;
}

The Rails app then accepts or rejects the requests based on the IP address in X-Real-IP header.