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).