• How To Get The Client IP Address In Your App

    by Sergei Kovalenko

    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:

    Java (via HttpServletRequest ):
    String clientIp = request.getRemoteAddr();

    PHP (via the $_SERVER superglobal):
    $clientIp = $_SERVER['REMOTE_ADDR'];

    NodeJs (via the Express request object):
    const clientIp = request.connection.remoteAddress;

    Pure NodeJs (via the 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:

    Java (via 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();
    }

    PHP (via the $_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'];
    }

    NodeJs (via the Express 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:

    2014, https://tools.ietf.org/html/rfc7239
    Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43