ip.wtf and showing you your actual HTTP request

I've been running a site to show you your IP address for a while.

I originally wrote it because I was doing some crazy networking tricks where the source port mattered (maybe another post on that one day), but none of the "what's my IP address" sites seemed to actually give me the source port. One even appeared to show the source port, but it wasn't right (presumably they added a reverse proxy at some point).

I also wanted to show you your actual HTTP request, exactly as sent to the server (i.e. a proxy on your side could change it, but not exposing any internal load balancing details on the hosting side).

Getting the raw HTTP request is fairly simple with Go (at least for HTTP/1.1), we can take advantage of interfaces and implement wrappers around net.Listener and net.Conn that store the data that is read, then make it available to the higher level HTTP code via the context.

Ignoring how the application queries the data it can be done with something like this:

// RecordingListener wraps a net.Listener and wraps the resulting accepted
// connections in a RecordingConn.
type RecordingListener struct {
        net.Listener
}

func (l RecordingListener) Accept() (net.Conn, error) {
        rw, err := l.Listener.Accept()
        return RecordingConn{Conn: rw, read: &readInfo{}}, err
}

// RecordingConn wraps a net.Conn and records the data returned by Read.
type RecordingConn struct {
        net.Conn
        read   *readInfo
}

type readInfo struct {
        read  []byte
        count int
}

func (c RecordingConn) Read(b []byte) (int, error) {
        // Forward to the underlying Read method.
        n, err := c.Conn.Read(b)
        if err != nil {
                return n, err
        }
        // Store the data returned by Read.
        c.read.read = append(c.read.read, b[:n]...)
        return n, err
}

For the full code see main.go on GitHub.

The problem with this is it implies there is not a HTTP proxy in front of the service, as that would change the request and it wouldn't be possible to show you the actual request as sent. I did host this on a dedicated IP address for a while, but given IPv4 address shortage is a thing, I want to be able to put this behind a proxy and serve other sites; on which I want to support HTTP/2 or other things seemingly incompatible with these low level needs. Enter haproxy.

The last time I wrote about haproxy was on how to use SSH and HTTPS on the same port, at the time it didn't natively support SSL, now it supports SSL and even HTTP/2 and many other new things. The flexibility it can give you really is quite amazing.

In this case the flexibility we want can be provided by using TCP content inspection (how the SSH trick also works) as well as switch-mode http. At its most basic, a configuration of:

frontend tcp-http
  # TCP mode, i.e. not HTTP
  mode tcp
  default_backend ipwtf
  acl ipwtf hdr(Host) -m end ip.wtf
  tcp-request inspect-delay 10s

  # The key line: If the acl "ipwtf" did not match, i.e. the host
  # isn't "ip.wtf", switch to HTTP mode.
  tcp-request content switch-mode http if !ipwtf
  # Otherwise use the "other" backend, which is HTTP.
  use_backend other if !ipwtf

backend ipwtf
  mode tcp
  default-server send-proxy-v2
  server ipwtf [::1]:8080

backend other
  mode http
  # some HTTP server here...

Means that if the host matches "ip.wtf" it will not switch to HTTP and instead proxy the entire TCP connection to the ipwtf backend. It will also wrap the connection in the PROXY v2 protocol so the backend can see what the source IP address is, without relying on HTTP headers (as that would change the request). This is pretty neat.

The actual configuration is a little more complex, as it does this over SSL too and uses the crt-list feature in haproxy to offer HTTP/2 via ALPN on some certificates (and keeping ip.wtf to HTTP/1.1 for now, as HTTP/2 is binary and not easy to display back to the user).

In order to do this in a "crt-list" file it has something like:

/etc/haproxy/ssl/ip.wtf.pem
/etc/haproxy/ssl/dgl.cx.pem [alpn h2,http/1.1]

So that the "ip.wtf" certificate does not offer HTTP/2 over ALPN, but the "dgl.cx" one does. It's important these be separate certificates as otherwise HTTP/2 connection coalescing would mean other hosts could be tried over the same connection, breaking our expectation of HTTP/1.1 only for "ip.wtf".

One noticeable aspect of haproxy's HTTP mode is for HTTP/2 reasons it lowercases headers, so we can see whether we are using haproxy's HTTP mode with curl:

$ curl -i ip.wtf
HTTP/1.1 200 OK
Access-Control-Allow-Methods: GET, OPTIONS, HEAD
[...snip...]

...which is not using HTTP mode, compared to:

$ curl -i dgl.cx
HTTP/1.1 301 Moved Permanently
content-length: 0
location: https://dgl.cx/

...this site, which is using HTTP mode (and supports HTTP/2). The other advantage of all these tricks is silly easter eggs like curl ip.wtf/moo work. Have fun.