VHost specific requests for load balanced services in Go

A couple of weeks ago I wrote about how to do VHost specific requests for load balanced services using Ethon. Our code base is written in Ruby so it was only natural to look for a solution that fits right in.
To summarize the intention behind something like that: I wanted (and needed) to be able to make requests to a host via TLS with SNI which has a VHost responding to a domain that does not actually (directly) point to the specific host.

In my spare time I am playing around with Go and I was wondering how to do it there.

HTTP requests to a very specific VHost can be made rather easily by just setting the host header to the domain and making a request to the IP:

req, _ := http.NewRequest("GET", "http://10.10.10.10", nil)  
req.Host = "api.example.com"  
client := http.Client{}  
resp, _ := client.Do(req)  
defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body)  
fmt.Println(string(body))  

This probably also works for (amongst others) SSLv3, which noone should be using anymore unless they want to be bitten by a POODLE.

For HTTPS requests encrypted with TLS and SNI this is slightly more complicated.
A few weeks ago while working on a side project I figured out how to just load the certificate of a specific VHost. This is rather easy: open up a TLS connection and after a successful dial up the certificate will be available.

config := tls.Config{  
  InsecureSkipVerify: true,
}

conn, _ := tls.Dial("tcp", "api.example.com:443", &config)  
defer conn.Close()

state := conn.ConnectionState()  
certs := state.PeerCertificates  
cert := *certs[0]

fmt.Println(cert.Subject.CommonName)  

This does work great if the domain actually does point to the host you want to address and does not just answer to a specific VHost.
If that is the case you can load the certificate by specificing the ServerName in the tls.Config and passing the IP of your host on dial up:

//...
config := tls.Config{  
    ServerName:         "api.example.com",
    InsecureSkipVerify: true,
}
conn, _ := tls.Dial("tcp", "10.10.10.10:443", &config)  
//...

Although knowing this did give me a hint into the direction I had to keep looking I was not quite there yet. Having the certificate and actually making an HTTP(S) request are two entirely different things.

The first thing I tried was looking at Go's great standard library and figuring out a way to send an HTTP request over an already established connection.
In general I guess this is possible. I would have had to write the whole code to make the request myself though.

After that I figured that there would have to be something similar to the way it is done in curl and Ethon which would allow me to skip resolving the domain name.

I came up with the following piece of code:

config := tls.Config{  
    ServerName:         "api.example.com",
    InsecureSkipVerify: true,
}

tr := &http.Transport{  
    TLSClientConfig: &config,
}
client := &http.Client{Transport: tr}  
req, _ := http.NewRequest("GET", "https://10.10.10.10", nil)  
resp, _ := client.Do(req)  
defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body)  
fmt.Println(string(body))  

I figured: the tls.Config should provide the correct ServerName for SNI to not complain so that the correct certificate would be returned.
The IP instead of a specific domain should ensure that the connection is made to the correct host.
Despite that I was not quite there yet, because I kept getting:

Your browser sent a request that this server could not understand.

as a response.

As I tried a couple of different things and ran out of ideas I turned to a usually very helpful resource in all terms Go: the golang-nuts google group.
I asked for a hint and my mistake was pointed out to me:
I forgot to set the host header (which I actually did for the non-encrypted version):

req.Host = "api.example.com"  

Adding that to the code snippet above

config := tls.Config{  
    ServerName:         "api.example.com",
    InsecureSkipVerify: true,
}

tr := &http.Transport{  
    TLSClientConfig: &config,
}
client := &http.Client{Transport: tr}  
req, _ := http.NewRequest("GET", "https://10.10.10.10", nil)  
req.Host = "api.example.com"  
resp, _ := client.Do(req)  
defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body)  

solved the issue and allowed me to make requests to a server which has a VHost responding to a specific domain without the domain actually directly pointing there (meaning: resolving the domain to its IP address would not have lead to the specific host).

comments powered by Disqus