Pretty often, your server app needs to know the IP address of a client making the HTTP request. For either securing the session, or tracking, or geolocating or any other reasonable purpose. All the programming languages give you their options to do that:
HttpServletRequest
):
String clientIp = request.getRemoteAddr();
$_SERVER
superglobal):
$clientIp = $_SERVER['REMOTE_ADDR'];
request
object):
const clientIp = request.connection.remoteAddress;
http.createServer()
's callback):
(req, res) => {
const clientIp = res.socket.remoteAddress;
}
As you can see, the server, listening to a socket in your app,
knows its direct client address and ready to share it with you. This
always works pretty well on your machine and gives you proper results:
you'd either see the local 127.0.0.1
address, or, if you run the app in a docker container, it'd be
the docker host machine address ::ffff:172.17.0.1
or smth.
Most of the internet guides stop at this point, as if that was it. But the interesting part comes when you deploy that on production.
Nobody wants to take care of load balancing, bot attack protection, caching and serving SSL certificates in their code every time. That's why your app on production is always deployed behind a load balancer, that serves requests of a cache manager, that sits behind a firewall, etc. And all those smart internet providers, hiding their client networks by chains of proxy-servers... What one IP from those is visible to your app? Its direct client – the load balancer, in this case.
Wait a minute! Does that mean, the client IP vanished into
oblivion?
Luckily, not.
By convention, every
proxy / firewall / load balancing server
should append an additional header to the request and store its client IP
there. You can find a lot of suggestions in the Internet, to take
the header called X-Forwarded-For
as
the client IP address. But that is just half true, which is worse than
a lie.
In fact, the X-Forwarded-For
header may contain the full chain of proxies, and its format is
a comma-separated list:
<client-ip>, <proxy1-ip>, <proxy2-ip>, ..., <proxyN-ip>
So, knowing that, we're ready to get the client IP address in a proper way:
HttpServletRequest
):
public String getClientIp(HttpServletRequest request)
{
String clientIp = request.getHeader('X-Forwarded-For');
if (clientIp != null)
{
clientIp = trim(clientIp.split(',')[0]);
if (isValidIp(clientIp))
{
return clientIp;
}
}
return request.getRemoteAddr();
}
$_SERVER
superglobal):
public function getClientIp() {
// Note the header name format
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$clientIp = trim($ips[0]);
if (isValidIp($clientIp)) {
return $clientIp;
}
}
return $_SERVER['REMOTE_ADDR'];
}
request
object):
const getClientIp = (request) => {
// header names are in lowercase
if (request.headers['x-forwarded-for']) {
const clientIp =
request
.headers['x-forwarded-for']
.split(',')[0]
.trim();
if (isValidIp(clientIp)) {
return clientIp;
}
}
return request.connection.remoteAddress;
}
Note, that there is a method isValidIp()
used in every snippet; all languages provide their own tools for such
a validation. Every time you get an IP from a request header, it must
be validated, since it's almost the same as a user input and can
contain anything. If the IP is not valid, better to fall back to
the remoteAddress
approach, or any string
constant of your choice, like
"127.0.0.1"
or
"::1"
.
Is that all? Are those snippets gonna work now?
Well, yes. If you're lucky and your proxy servers are properly
configured to follow the convention and store the client IP in the
X-Forwarded-For
header. If you can
configure them or ask someone to do that for you, always prefer
that way.
Otherwise, you’ll have to experiment in the target environment
and start listing all the request headers. There you may find a whole zoo
of them being appended. They can be called
X-Real-IP
,
Client-Real-IP
,
pRO-X-yEnCipHereDNAme-Client-IP
, etc.
Actually those names are only limited by their server owner's imagination.
If you'd like (or you have to), you can take those headers into
consideration too:
// ...
const weirdHeaderNames = [ // order by priority
'x-real-ip',
'client-real-ip',
// ...
]
foreach (headerName in weirdHeaderNames) {
if (headerExists(headerName)) {
clientIp = extractIpFromHeader(headerName);
if (isValidIp(clientIp)) {
return clientIp
}
}
}
// ...
Translate this to your favorite programming language and
put after checking the X-Forwarded-For
header and before returning the default value at the end of the method.
And don't forget to add a big comment explaining those headers and
the sources where they are coming from. Otherwise, the next developer
will never know what can be kept and what should be thrown away.
For the future, you may consider processing also
the Forwarded
header, that was proposed
in 2014 but not actively used IRL yet. Probably, due to its complexity
and overload:
Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43