The way Ruby’s net/http library exposes customization of SNI (Sever Name Indication) is quite counterintuitive at the first sight. It is also pretty much not documented. I discovered it through this closed feature request and the relevant source code.
In this article I’m going to share two small code snippets on how to do that:
(1) with net/http directly (2) with Faraday gem. Let’s assume our domain is
and we want to force HTTPS requests to it to
Directly with net/http
require 'net/http' uri = URI('https://example.com') Net::HTTP.start(uri.host, uri.port, use_ssl: true, ipaddr: 'forced-endpoint.com') do |http| request = Net::HTTP::Get.new(uri) response = http.request(request) p response.code p response.each_header.to_h.to_s end
Using Faraday gem
require 'faraday' conn = Faraday.new('https://example.com') do |f| f.adapter :net_http do |http| http.ipaddr = 'forced-endpoint.com' end end response = conn.get('/') p response.status
Both code snippet will result in TCP connection to
forced-endpoint.com, where TLS handshake is done
example.com as SNI while HTTP Host header is set to
In both cases the key is setting
ipaddr attribute. One can also set it to a specific IP address.
That is where the counterintuitiveness of this (at the first sight) configuration comes from:
you have to mention address for TCP/socket layer in order to force net/http to use provided host as SNI.
What would seemingly be more intuitive is to explicitly set SNI, for example that is how Go does it
TLSClientConfig.ServerName attribute. However if you think about it a bit more, Ruby’s approach is arguably better!
The author of the related patch explains it in this comment as to why
he chose to expose overriding of
ipaddr for underlying TCP connection instead of the SNI.
The tldr; is that when you override SNI, most of the times you also want to override HTTP
so that they are the same - that is also how
curl with its
--resolve argument works.
comments powered by Disqus