像主机文件一样覆盖特定域的 DNS,但不使用主机文件

Override DNS For Specific Domains Like A Hosts File, But Without Using Hosts file

我需要从同一台服务器向特定域发出一系列并行 Web 请求,但要控制这些请求实际转到的 IP 地址。最初,我想出了一个方案,我会专门请求我想要的 IP,然后在请求上手动设置 Host: www.example.com header,并使用一系列处理程序来确保发出重定向相同的模式。

这似乎工作了一段时间,但最近我在重定向到 HTTPS 时遇到了问题。握手会失败,然后再请求。 我尝试过多种方式禁用 SSL 验证,包括:

local $ENV{ PERL_LWP_SSL_VERIFY_HOSTNAME } = 0;
local $ENV{ HTTPS_DEBUG }                  = 1;

$ua->ssl_opts(
    SSL_ca_file => Mozilla::CA::SSL_ca_file(),
    verify_hostname => 0,
    SSL_verify_mode => 0x00,
);

IO::Socket::SSL::set_ctx_defaults(
    SSL_verifycn_scheme => 'www',
    SSL_verify_mode => 0,
);        

我也试过使用 LWP::UserAgent::DNS::Hosts 来解决问题,但它仍然存在。

我应该注意到,关闭 SSL 的对等验证不能解决问题的原因可能是因为出于某种原因请求这种方式实际上导致握手失败,而不是失败在验证点上。

一件可行的事情是在 /etc/hosts 中创建一个条目以将域指向适当的 IP,但这不切实际,因为我可能需要 运行 数十或数百个在同一个域上并行测试。

有没有一种方法可以模拟向 /etc/hosts 添加条目的功能,而不涉及专门请求 IP 和覆盖 Host: ... HTTP header?

编辑:SSL 调试信息

DEBUG: .../IO/Socket/SSL.pm:1914: new ctx 140288835318480
DEBUG: .../IO/Socket/SSL.pm:402: socket not yet connected
DEBUG: .../IO/Socket/SSL.pm:404: socket connected
DEBUG: .../IO/Socket/SSL.pm:422: ssl handshake not started
DEBUG: .../IO/Socket/SSL.pm:455: not using SNI because hostname is unknown
DEBUG: .../IO/Socket/SSL.pm:478: set socket to non-blocking to enforce timeout=180
DEBUG: .../IO/Socket/SSL.pm:491: Net::SSLeay::connect -> -1
DEBUG: .../IO/Socket/SSL.pm:501: ssl handshake in progress
DEBUG: .../IO/Socket/SSL.pm:511: waiting for fd to become ready: SSL wants a read first
DEBUG: .../IO/Socket/SSL.pm:531: socket ready, retrying connect
DEBUG: .../IO/Socket/SSL.pm:491: Net::SSLeay::connect -> -1
DEBUG: .../IO/Socket/SSL.pm:1388: SSL connect attempt failed with unknown error

DEBUG: .../IO/Socket/SSL.pm:497: fatal SSL error: SSL connect attempt failed with unknown error error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure
DEBUG: .../IO/Socket/SSL.pm:1948: free ctx 140288835318480 open=140288835318480
DEBUG: .../IO/Socket/SSL.pm:1953: free ctx 140288835318480 callback
DEBUG: .../IO/Socket/SSL.pm:1956: OK free ctx 140288835318480

在回复中我得到:

Can't connect to redacted.org:443

SSL connect attempt failed with unknown error error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure at /System/Library/Perl/Extras/5.18/LWP/Protocol/http.pm line 51.

它在我们的服务器上同样失败(使用较旧的遗留版本的 Perl,我不会在这里透露它似乎无关紧要)。

服务器最初通过 301 重定向到 HTTPS 站点来响应 non-HTTPS 请求。然后发生故障。我将 post 重现代码并删除我的请求的具体细节,但任何将 non-HTTPS 流量重定向到 HTTPS 的站点都应该足够了。

use IO::Socket::SSL qw/ debug4 /;
use LWP::UserAgent;
use LWP::UserAgent::DNS::Hosts;
use HTTP::Request;
use Mozilla::CA;
use Data::Dumper;

LWP::UserAgent::DNS::Hosts->register_hosts(
    'recacted.org' => '127.0.0.1', # no I am not redirecting to loopback in reality, this is anonymized
    'www.redacted.org' => '127.0.0.1',
);

LWP::UserAgent::DNS::Hosts->enable_override;

my $ua = LWP::UserAgent->new;
$ua->ssl_opts( SSL_ca_file => Mozilla::CA::SSL_ca_file() );

my $request = HTTP::Request->new(GET => 'http://redacted.org/');

my $response = $ua->request($request);

print $response->content; #Dumper ( $response->is_success ? $response->headers : $response );

同样,这不是生产代码,只是足以重现问题的代码。它似乎与 SSL 验证没有任何关系,而且无法协商请求,大概是因为 LWP::UserAgent::DNS::Hosts 正在做我正在做的事情:将请求目标更改为所需的 IP,然后写入Host: ... header 手动。为什么这会导致SSL握手失败,我不知道。

在我的本地机器上调试

openssl version -a: 1.0.2j 26 Sep 2016
IO::Socket::SSL->VERSION == 1.966
Net::SSLeay->VERSION == 1.72

在我们的服务器上

openssl version -a: 1.0.1t 3 May 2016
IO::Socket::SSL->VERSION == 1.76
Net::SSLeay->VERSION == 1.48

鉴于它适用于显式 /etc/hosts 文件,但不能仅替换 PeerAddr 或使用 LWP::UserAgent::DNS::Hosts 这看起来像是 SNI 扩展的问题。此 TLS 扩展用于向 TLS 服务器提供请求的主机名(类似于 HTTP Host header),以便它可以选择合适的证书。如果此 SNI 扩展缺少一些服务器 return 默认证书,而其他服务器则抛出错误,就像在这种情况下。

解决方法是在 ssl_opts 中使用 SSL_hostname 提供主机名。这种修复可能也有助于 LWP::UserAgent::DNS::Hosts,即 LWP/Protocol/https/hosts.pm:

12    if (my $peer_addr = LWP::UserAgent::DNS::Hosts->_registered_peer_addr($host)) {
13        push @opts, (
14            PeerAddr          => $peer_addr,
15            Host              => $host,
16            SSL_verifycn_name => $host,
NEW           SSL_hostname      => $host,   # for SNI
17        );
18    }