1. 68
  1. 24

    I see too many people rolling PHP-FPM only to show an IP address to the client. So, I wanted to share a simpler method which, I hope, can save you some time.

    1. 3

      This seems very elegant, but just to be thorough are there any drawbacks/tradeoffs?

      1. 4

        The same T&Cs apply as when using nginx for standard stuff. This will/might be wrong if this nginx is behind another nginx, then you should look at X_FORWARDED_FOR (or whatever it’s called exactly).

        1. 2

          Beware if you use another public facing server in front of nginx. For example, if you have a reverse proxy (HAproxy for example), then the variable $remote_addr can represent the IP address of the proxy, not the initial HTTP client.

          1. 4

            Have a look at the realip module that allows Nginx to set the remote address based on a header set by the frontend proxy provided it’s one you have decided to trust that it’s setting correct headers.

            Doing this over a custom solution in the application has the advantage that all remote address based features continue to work unaltered. Like geoip detection or logging addresses to web log files using built-in standard formats

        2. 2

          Yes this method also has my preference and we use that for years now. Years ago we used to use php (without php-fpm) for this, more or less like this:

          <?php
          echo $_SERVER['REMOTE_ADDR'] . PHP_EOL;
          

          But I was wondering: do you have suggestions for making it output both IPv4 and IPv6 addresses (like https://ip6.nl and others do) without adding additional complexity/dependencies like php (preferably with stock nginx or apache).

          1. 4

            To show both IPv4 and IPv6, the client needs to make two separate requests, to two separate domains that are configured differently, one with only an A record and one with only an AAAA record. Any given HTTP request is only going to be coming in on one or the other IP version.

            ip6.nl makes XHR requests to https://4only.ip6.nl/myip.plp and https://6only.ip6.nl/myip.plp and displays the results on the page, again with Javascript. While those servers could very well be running the nginx config in the linked article, the ability to show both on the same page is much more complicated, tech-wise.

            1. 2

              You might be able to do it with redirects. Have the IPv4 server redirect to the IPv6 server with ?v4=a.b.c.d, and vice versa. Both servers would display both addresses once available.

              It falls apart if you only have one type of address, since the redirect would be broken, but there’s probably a way around that. Maybe include the single address in the body of the 303, so if the redirect fails to connect you still have the initial IP address you used?

              1. 3

                The case where the caller can only connect on one protocol is probably very, very common still.

            2. 3

              But I was wondering: do you have suggestions for making it output both IPv4 and IPv6 addresses (like https://ip6.nl and others do) without adding additional complexity/dependencies like php (preferably with stock nginx or apache).

              The tcp/ip stack of the client decides whether to try to connect using v4 or v6 first. I’ve added two extra dns entries, one with only a v4 address, and one with only a v6 address atop of one that has both: http://ip.netsend.nl http://ip4.netsend.nl http://ip6.netsend.nl

            3. 2

              Nice trick! Thanks!

              However, you could add links to the relevant nginx-pages to your blog post as well.

            4. 4

              You can just put this in your HTML if you have SSI enabled in your webserver…

              <!--#echo var="remote_addr"-->
              

              I use it on my dumb landing page: https://feld.me

              1. 4

                Do you think you could come up with an nginx config that responds with text/plain or application/json conditionally depending on the Accept-Encoding request headers?

                1. 9

                  Something like this? (I haven’t tested it, though.)

                  location /ip {
                      if ($http_accept ~ "application/json") {
                          default_type application/json;
                          return 200 "{\"ip\":\"$remote_addr\"}";
                      }
                      default_type text/plain;
                      return 200 $remote_addr;
                  }
                  

                  Note that you want the Accept HTTP header, Accept-Encoding specifies mostly the compression used in transit.

                  Hmm, and on closer look, the docs say that default_type is not valid in an if block, so if the above doesn’t work, maybe this will:

                  location /ip {
                      if ($http_accept ~ "application/json") {
                          add_header Content-Type application/json;
                          return 200 "{\"ip\":\"$remote_addr\"}";
                      }
                      default_type text/plain;
                      return 200 $remote_addr;
                  }
                  

                  One more edit.

                  This turns out to be surprisingly tricky. The first version doesn’t work, for the reason described above. The second version sort of works, but for a JSON request, it returns the Content-Type header twice – for some reason, nginx insists on including the default_type even if you explicitly set the header with add_header. One workaround seems to be to install https://github.com/openresty/headers-more-nginx-module, and use more_set_headers, but that requires a rebuild of nginx. If someone has a properly working version with just plain nginx, I’d be interested, but it seems much easier to just define a separate location, like /ip.json.

                  1. 3

                    I realized this using custom error pages and named locations, like this:

                    location ^~ /ip {
                        error_page 463 = @ip;
                        error_page 464 = @ip_json;
                    
                        if ($arg_json) {
                            return 464;
                        }
                        if ($http_accept = 'application/json') {
                            return 464;
                        }
                    
                        return 463;
                    }
                    
                    location @ip {
                        default_type text/plain;
                    
                        return 200 $remote_addr;
                    }
                    
                    location @ip_json {
                        default_type application/json;
                    
                        return 200 "{ \"ip\": \"$remote_addr\" }";
                    }
                    
                    1. 2

                      Ah, of course, I should have thought of internal redirects. And the trick with using a custom error code to force an internal redirect is also quite neat, I didn’t know that one. Thanks!

                      And I’m guessing it would be possible to simplify this by just directly using the default_type and return directive of @ip in the first location block; the default shouldn’t need an internal redirect.