Prechádzať zdrojové kódy

Add set_tunnel_hostname_addr_map() and deprecate set_hostname_addr_map() (#2647)

Through a CONNECT proxy it is the proxy that connects to the origin, and the CONNECT target was always the original hostname, so there was no way to send the tunnel to another address. set_tunnel_hostname_addr_map() sets that target on SSLClient, Client and WebSocketClient. The CONNECT request's Host follows it, including the Digest retry, while the Host header, the SNI and the certificate check inside the tunnel keep the original hostname. The map is also copied to a redirect client.

set_hostname_addr_map() is deprecated in favor of set_direct_hostname_addr_map(), which behaves the same and names the connection it applies to. The two maps are independent and correspond to curl's --resolve and --connect-to; neither applies to the proxy's own address.
Kim, Hyuk 21 hodín pred
rodič
commit
290c4c0bbe

+ 2 - 1
README-websocket.md

@@ -233,7 +233,8 @@ void set_address_family(int family);
 void set_ipv6_v6only(bool on);
 void set_socket_options(SocketOptions socket_options);
 void set_interface(const std::string &intf);
-void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+void set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
+void set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
 
 // SSL configuration (wss:// only, SSL builds only)
 void set_ca_cert_path(const std::string &ca_cert_file_path,

+ 32 - 8
README.md

@@ -1477,17 +1477,17 @@ The same method is available on `httplib::ws::WebSocketClient`.
 
 ### Override the connection target for a hostname
 
-`set_hostname_addr_map` redirects where the socket connects, without changing
-the identity of the request. The hostname the client was constructed with keeps
-supplying the `Host` header, the SNI, and the name that the server certificate
-is verified against, so this is a connection-level override only, not a way to
-talk to a different origin.
+`set_direct_hostname_addr_map` redirects where the socket connects, without
+changing the identity of the request. The hostname the client was constructed
+with keeps supplying the `Host` header, the SNI, and the name that the server
+certificate is verified against, so this is a connection-level override only,
+not a way to talk to a different origin.
 
 ```cpp
 httplib::Client cli("https://example.com");
 
 // Connect to this IP address instead of resolving "example.com"
-cli.set_hostname_addr_map({{"example.com", "192.168.1.10"}});
+cli.set_direct_hostname_addr_map({{"example.com", "192.168.1.10"}});
 ```
 
 A mapped value may be an IP literal or another hostname. An IP literal is used
@@ -1495,13 +1495,37 @@ as-is; anything else is resolved as a name, so a host that is only reachable
 under a different name works too:
 
 ```cpp
-cli.set_hostname_addr_map({{"example.com", "internal.example.lan"}});
+cli.set_direct_hostname_addr_map({{"example.com", "internal.example.lan"}});
 ```
 
 An empty value is ignored, leaving the original hostname as the connection
 target.
 
-The same method is available on `httplib::ws::WebSocketClient`.
+The direct map applies only when the client connects to the origin itself.
+Through a proxy, use `set_tunnel_hostname_addr_map`: its value becomes the
+`CONNECT` target (and that request's `Host`) that the proxy is asked to connect
+to (HTTPS and WebSocket). The `Host` header, the SNI and the certificate check
+inside the tunnel keep the original hostname:
+
+```cpp
+httplib::Client cli("https://sni.example");
+
+cli.set_tunnel_hostname_addr_map({{"sni.example", "server.example"}});
+cli.set_proxy("proxy.example", 3128);
+// CONNECT server.example:443, SNI sni.example
+```
+
+The two maps are independent; to use the same target on both paths, set both.
+They correspond to curl's `--resolve` and `--connect-to`.
+
+Neither map applies to the proxy's own address ("direct" means not through a
+proxy, as in a PAC file's `DIRECT`). To connect to the proxy at a fixed
+address, pass an IP literal to `set_proxy()`.
+
+`set_hostname_addr_map` is deprecated and works the same as
+`set_direct_hostname_addr_map`.
+
+These methods are also available on `httplib::ws::WebSocketClient`.
 
 ### Automatic Path Encoding
 

+ 1 - 1
docs-src/pages/en/tour/09-whats-next.md

@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80);  // The port is unused, but must not be 0
 // Client
 httplib::Client cli("http://localhost");
 cli.set_address_family(AF_UNIX);
-cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
+cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
 
 auto res = cli.Get("/");
 ```

+ 1 - 1
docs-src/pages/ja/tour/09-whats-next.md

@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80);  // ポート番号は使われない(0
 // クライアント
 httplib::Client cli("http://localhost");
 cli.set_address_family(AF_UNIX);
-cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
+cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
 
 auto res = cli.Get("/");
 ```

+ 74 - 6
httplib.h

@@ -2927,7 +2927,12 @@ public:
   size_t is_socket_open() const;
   socket_t socket() const;
 
+  [[deprecated("Use set_direct_hostname_addr_map() instead.")]]
   void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+  void
+  set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
+  void
+  set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
 
   void set_default_headers(Headers headers);
 
@@ -3059,6 +3064,9 @@ protected:
   // hostname; only the connection target changes, never the identity.
   std::map<std::string, std::string> addr_map_;
 
+  // Hostname to CONNECT target map, used only through a proxy.
+  std::map<std::string, std::string> tunnel_addr_map_;
+
   // Default headers
   Headers default_headers_;
 
@@ -3348,7 +3356,12 @@ public:
   size_t is_socket_open() const;
   socket_t socket() const;
 
+  [[deprecated("Use set_direct_hostname_addr_map() instead.")]]
   void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+  void
+  set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
+  void
+  set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
 
   void set_default_headers(Headers headers);
 
@@ -4748,7 +4761,12 @@ public:
   set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
 
   void set_interface(const std::string &intf);
+  [[deprecated("Use set_direct_hostname_addr_map() instead.")]]
   void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+  void
+  set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
+  void
+  set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
 
   void set_proxy(const std::string &host, int port);
   void set_proxy_basic_auth(const std::string &username,
@@ -4820,6 +4838,9 @@ private:
   // hostname; only the connection target changes, never the identity.
   std::map<std::string, std::string> addr_map_;
 
+  // Hostname to CONNECT target map, used only through a proxy.
+  std::map<std::string, std::string> tunnel_addr_map_;
+
   std::string proxy_host_;
   int proxy_port_ = -1;
   std::string proxy_basic_auth_username_;
@@ -13046,6 +13067,17 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
   return prepare_host_string(host) + ":" + std::to_string(port);
 }
 
+// Request target of a CONNECT for `host`, with `addr_map` applied.
+inline std::string
+make_connect_request_target(const std::map<std::string, std::string> &addr_map,
+                            const std::string &host, int port) {
+  std::string connect_host;
+  std::string ip;
+  apply_addr_map(addr_map, host, connect_host, ip);
+  return make_host_and_port_string_always_port(ip.empty() ? connect_host : ip,
+                                               port);
+}
+
 // Value for the Host header a client sends when the caller supplied none.
 // Only the value: callers decide where in their header list it goes.
 inline std::string make_default_host_header_value(const std::string &host,
@@ -16574,6 +16606,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
 
   // Copy network and socket settings
   client.addr_map_ = addr_map_;
+  client.tunnel_addr_map_ = tunnel_addr_map_;
   client.set_address_family(address_family_);
   client.set_tcp_nodelay(tcp_nodelay_);
   client.set_ipv6_v6only(ipv6_v6only_);
@@ -18086,9 +18119,19 @@ inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
 
 inline void
 ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
+  set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+inline void ClientImpl::set_direct_hostname_addr_map(
+    std::map<std::string, std::string> addr_map) {
   addr_map_ = std::move(addr_map);
 }
 
+inline void ClientImpl::set_tunnel_hostname_addr_map(
+    std::map<std::string, std::string> addr_map) {
+  tunnel_addr_map_ = std::move(addr_map);
+}
+
 inline void ClientImpl::set_default_headers(Headers headers) {
   default_headers_ = std::move(headers);
 }
@@ -18797,7 +18840,17 @@ inline socket_t Client::socket() const { return cli_->socket(); }
 
 inline void
 Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
-  cli_->set_hostname_addr_map(std::move(addr_map));
+  cli_->set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+inline void Client::set_direct_hostname_addr_map(
+    std::map<std::string, std::string> addr_map) {
+  cli_->set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+inline void Client::set_tunnel_hostname_addr_map(
+    std::map<std::string, std::string> addr_map) {
+  cli_->set_tunnel_hostname_addr_map(std::move(addr_map));
 }
 
 inline void Client::set_default_headers(Headers headers) {
@@ -19165,6 +19218,10 @@ inline bool SSLClient::connect_with_proxy(
     std::chrono::time_point<std::chrono::steady_clock> start_time,
     Response &res, bool &success, Error &error) {
   success = true;
+  auto connect_target =
+      detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
+  auto retargeted = connect_target !=
+                    detail::make_host_and_port_string_always_port(host_, port_);
   Response proxy_res;
   if (!detail::process_client_socket(
           socket.sock, read_timeout_sec_, read_timeout_usec_,
@@ -19172,8 +19229,8 @@ inline bool SSLClient::connect_with_proxy(
           start_time, [&](Stream &strm) {
             Request req2;
             req2.method = "CONNECT";
-            req2.path =
-                detail::make_host_and_port_string_always_port(host_, port_);
+            req2.path = connect_target;
+            if (retargeted) { req2.set_header("Host", connect_target); }
             if (max_timeout_msec_ > 0) {
               req2.start_time_ = std::chrono::steady_clock::now();
             }
@@ -19212,8 +19269,8 @@ inline bool SSLClient::connect_with_proxy(
                 start_time, [&](Stream &strm) {
                   Request req3;
                   req3.method = "CONNECT";
-                  req3.path = detail::make_host_and_port_string_always_port(
-                      host_, port_);
+                  req3.path = connect_target;
+                  if (retargeted) { req3.set_header("Host", connect_target); }
                   req3.headers.insert(detail::make_digest_authentication_header(
                       req3, make_request_target(req3), auth, 1,
                       detail::random_string(10), proxy_digest_auth_username_,
@@ -24058,9 +24115,19 @@ inline void WebSocketClient::set_interface(const std::string &intf) {
 
 inline void WebSocketClient::set_hostname_addr_map(
     std::map<std::string, std::string> addr_map) {
+  set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+inline void WebSocketClient::set_direct_hostname_addr_map(
+    std::map<std::string, std::string> addr_map) {
   addr_map_ = std::move(addr_map);
 }
 
+inline void WebSocketClient::set_tunnel_hostname_addr_map(
+    std::map<std::string, std::string> addr_map) {
+  tunnel_addr_map_ = std::move(addr_map);
+}
+
 inline bool WebSocketClient::is_proxy_enabled() const {
   if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
   if (no_proxy_entries_.empty()) { return true; }
@@ -24110,7 +24177,8 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
                                                 Error &error) {
   Request req;
   req.method = "CONNECT";
-  req.path = detail::make_host_and_port_string_always_port(host_, port_);
+  req.path =
+      detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
   req.headers.emplace("Host", req.path);
   detail::add_default_user_agent_header(req);
   detail::add_authorization_header(req, proxy_basic_auth_username_,

+ 289 - 20
test/test.cc

@@ -3748,7 +3748,7 @@ TEST(SpecifyServerIPAddressTest, AnotherHostname_Online) {
   Client cli(host);
 #endif
 
-  cli.set_hostname_addr_map({{another_host, wrong_ip}});
+  cli.set_direct_hostname_addr_map({{another_host, wrong_ip}});
   auto res = cli.Get("/");
   ASSERT_TRUE(res) << "Error: " << to_string(res.error());
   ASSERT_EQ(StatusCode::MovedPermanently_301, res->status);
@@ -3764,7 +3764,7 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) {
   Client cli(host);
 #endif
 
-  cli.set_hostname_addr_map({{host, wrong_ip}});
+  cli.set_direct_hostname_addr_map({{host, wrong_ip}});
   auto res = cli.Get("/");
   ASSERT_TRUE(!res);
   EXPECT_EQ(Error::Connection, res.error());
@@ -3795,7 +3795,7 @@ TEST(SpecifyServerIPAddressTest, HostnameAsAddrMapValue) {
   svr.wait_until_ready();
 
   Client cli(host, port);
-  cli.set_hostname_addr_map({{host, HOST}});
+  cli.set_direct_hostname_addr_map({{host, HOST}});
 
   auto res = cli.Get("/hi");
   ASSERT_TRUE(res) << "Error: " << to_string(res.error());
@@ -3825,7 +3825,7 @@ TEST(SpecifyServerIPAddressTest, IPAddressAsAddrMapValue) {
   svr.wait_until_ready();
 
   Client cli(host, port);
-  cli.set_hostname_addr_map({{host, "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{host, "127.0.0.1"}});
 
   auto res = cli.Get("/hi");
   ASSERT_TRUE(res) << "Error: " << to_string(res.error());
@@ -3858,13 +3858,73 @@ TEST(SpecifyServerIPAddressTest, EmptyAddrMapValueIsIgnored) {
   svr.wait_until_ready();
 
   Client cli(blackhole, port);
-  cli.set_hostname_addr_map({{blackhole, ""}});
+  cli.set_direct_hostname_addr_map({{blackhole, ""}});
   cli.set_connection_timeout(1);
 
   auto res = cli.Get("/hi");
   EXPECT_FALSE(res) << "empty mapping must not redirect to loopback";
 }
 
+TEST(SpecifyServerIPAddressTest, DeprecatedSetHostnameAddrMapStillWorks) {
+  auto host = "target.invalid";
+
+  Server svr;
+  std::string received_host;
+  svr.Get("/hi", [&](const Request &req, Response &res) {
+    received_host = req.get_header_value("Host");
+    res.set_content("Hello World!", "text/plain");
+  });
+
+  auto port = svr.bind_to_any_port("127.0.0.1");
+  auto thread = std::thread([&]() { svr.listen_after_bind(); });
+
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    thread.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+
+  svr.wait_until_ready();
+
+  Client cli(host, port);
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+  cli.set_hostname_addr_map({{host, "127.0.0.1"}});
+#pragma GCC diagnostic pop
+
+  auto res = cli.Get("/hi");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+  EXPECT_EQ(std::string(host) + ":" + std::to_string(port), received_host);
+}
+
+TEST(TunnelHostnameAddrMapTest, IgnoredOnDirectConnection) {
+  // 192.0.2.1 is TEST-NET-1: applying the map would fail to connect.
+  Server svr;
+  svr.Get("/hi", [](const Request & /*req*/, Response &res) {
+    res.set_content("Hello World!", "text/plain");
+  });
+
+  auto port = svr.bind_to_any_port("127.0.0.1");
+  auto thread = std::thread([&]() { svr.listen_after_bind(); });
+
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    thread.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+
+  svr.wait_until_ready();
+
+  Client cli("127.0.0.1", port);
+  cli.set_tunnel_hostname_addr_map({{"127.0.0.1", "192.0.2.1"}});
+  cli.set_connection_timeout(1);
+
+  auto res = cli.Get("/hi");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+}
+
 TEST(AbsoluteRedirectTest, Redirect_Online) {
   auto host = "httpbingo.org";
   auto path = std::string{"/absolute-redirect/3"};
@@ -13347,7 +13407,7 @@ TEST(SNI_AutoDetectionTest, FailedHandshakeLeavesNoSNI) {
   {
     // Sends SNI, then rejects the server certificate, which the CA did not sign
     SSLClient cli("localhost", port);
-    cli.set_hostname_addr_map({{"localhost", "127.0.0.1"}});
+    cli.set_direct_hostname_addr_map({{"localhost", "127.0.0.1"}});
     cli.set_ca_cert_path(CLIENT_CA_CERT_FILE);
     auto res = cli.Get("/sni");
     ASSERT_FALSE(res);
@@ -16436,7 +16496,7 @@ TEST(SSLClientServerTest, ClientCertNotSentToUnverifiedServer) {
       svr.wait_until_ready();
 
       SSLClient cli(c.host, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
-      cli.set_hostname_addr_map({{c.host, "127.0.0.1"}});
+      cli.set_direct_hostname_addr_map({{c.host, "127.0.0.1"}});
       cli.set_ca_cert_path(c.trusted_ca);
       if (with_verifier) {
         cli.set_server_certificate_verifier(
@@ -21530,6 +21590,36 @@ TEST(MakeHostAndPortStringTest, VariousPatterns) {
             detail::make_host_and_port_string("example.com", 65536, false));
 }
 
+namespace httplib {
+namespace detail {
+std::string
+make_connect_request_target(const std::map<std::string, std::string> &addr_map,
+                            const std::string &host, int port);
+} // namespace detail
+} // namespace httplib
+
+TEST(MakeConnectRequestTargetTest, FollowsHostnameAddrMap) {
+  std::map<std::string, std::string> addr_map = {
+      {"ipv4.example", "192.0.2.10"},
+      {"ipv6.example", "2001:db8::1"},
+      {"name.example", "internal.example.lan"},
+      {"empty.example", ""}};
+
+  EXPECT_EQ("192.0.2.10:443",
+            detail::make_connect_request_target(addr_map, "ipv4.example", 443));
+  EXPECT_EQ("[2001:db8::1]:443",
+            detail::make_connect_request_target(addr_map, "ipv6.example", 443));
+  EXPECT_EQ("internal.example.lan:8443", detail::make_connect_request_target(
+                                             addr_map, "name.example", 8443));
+
+  // An empty or absent mapping keeps the hostname as the target
+  EXPECT_EQ("empty.example:443", detail::make_connect_request_target(
+                                     addr_map, "empty.example", 443));
+  EXPECT_EQ("other.example:443", detail::make_connect_request_target(
+                                     addr_map, "other.example", 443));
+  EXPECT_EQ("[::1]:443", detail::make_connect_request_target({}, "::1", 443));
+}
+
 #ifdef CPPHTTPLIB_SSL_ENABLED
 TEST(SSLClientHostHeaderTest, Issue2301_Online) {
   httplib::SSLClient cli("roblox.com", 443);
@@ -27908,7 +27998,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_AnotherHostname) {
   auto wrong_ip = "192.0.2.1";
 
   ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
-  client.set_hostname_addr_map({{another_host, wrong_ip}});
+  client.set_direct_hostname_addr_map({{another_host, wrong_ip}});
 
   ASSERT_TRUE(client.connect());
   EXPECT_TRUE(client.is_open());
@@ -27932,7 +28022,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
   auto wrong_ip = "192.0.2.1";
 
   ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
-  client.set_hostname_addr_map({{"localhost", wrong_ip}});
+  client.set_direct_hostname_addr_map({{"localhost", wrong_ip}});
   client.set_connection_timeout(1);
   client.set_read_timeout(1);
   client.set_write_timeout(1);
@@ -27972,7 +28062,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_HostnameAsAddrMapValue) {
 
   ws::WebSocketClient client("ws://" + std::string(host) + ":" +
                              std::to_string(port) + "/ws");
-  client.set_hostname_addr_map({{host, HOST}});
+  client.set_direct_hostname_addr_map({{host, HOST}});
 
   ASSERT_TRUE(client.connect());
   EXPECT_TRUE(client.is_open());
@@ -30961,7 +31051,7 @@ private:
 inline std::unique_ptr<Client> make_client(const std::string &host,
                                            ProxyAndTargetServers &s) {
   auto cli = detail::make_unique<Client>(host, s.target_port());
-  cli->set_hostname_addr_map({{host, "127.0.0.1"}});
+  cli->set_direct_hostname_addr_map({{host, "127.0.0.1"}});
   cli->set_proxy("127.0.0.1", s.proxy_port());
   return cli;
 }
@@ -31170,7 +31260,7 @@ TEST(NoProxyTest, IPv4LiteralExactMatch) {
 TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) {
   ProxyAndTargetServers s;
   Client cli("0:0:0:0:0:0:0:1", s.target_port());
-  cli.set_hostname_addr_map({{"0:0:0:0:0:0:0:1", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"0:0:0:0:0:0:0:1", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", s.proxy_port());
   cli.set_no_proxy({"::1"});
 
@@ -31187,7 +31277,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
   // host as a hostname and miss the match.
   ProxyAndTargetServers s;
   Client cli("fe80::1", s.target_port());
-  cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", s.proxy_port());
   cli.set_no_proxy({"fe80::/10"});
 
@@ -31200,7 +31290,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
 TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
   ProxyAndTargetServers s;
   Client cli("::1", s.target_port());
-  cli.set_hostname_addr_map({{"::1", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"::1", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", s.proxy_port());
   cli.set_no_proxy({"[::1]"});
 
@@ -31213,7 +31303,7 @@ TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
 TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) {
   ProxyAndTargetServers s;
   Client cli("fe80::1", s.target_port());
-  cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", s.proxy_port());
   cli.set_no_proxy({"[fe80::]/10"});
 
@@ -31229,7 +31319,7 @@ TEST(NoProxyTest, IPv4MappedIPv6IsNotCrossMatchedAgainstIPv4Entry) {
   // tricks via address-family conversion.
   ProxyAndTargetServers s;
   Client cli("::ffff:127.0.0.1", s.target_port());
-  cli.set_hostname_addr_map({{"::ffff:127.0.0.1", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"::ffff:127.0.0.1", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", s.proxy_port());
   cli.set_no_proxy({"127.0.0.1"});
 
@@ -31445,7 +31535,7 @@ TEST(NoProxyTest, RedirectToBypassedHostStripsProxyAndProxyAuth) {
   target.listen();
 
   Client cli("public.example", target.port());
-  cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", proxy_mock.port());
   cli.set_proxy_basic_auth("u", "p");
   cli.set_no_proxy({"127.0.0.1"});
@@ -31481,7 +31571,7 @@ TEST(NoProxyTest, BypassedTargetReturning407DoesNotLeakProxyDigestCredentials) {
   // if it doesn't, the test still routes "via proxy" to the same server and
   // the Proxy-Authorization assertion below catches the leak.
   Client cli("evil.example", target.port());
-  cli.set_hostname_addr_map({{"evil.example", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"evil.example", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", target.port());
   cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
   cli.set_no_proxy({"evil.example"});
@@ -31532,7 +31622,7 @@ TEST(NoProxyTest, MultiHopRedirectThroughBypassedHostKeepsProxy) {
   bypass_server.listen();
 
   Client cli("public.example", public_port);
-  cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", proxy_mock.port());
   cli.set_no_proxy({"127.0.0.1"});
   cli.set_follow_location(true);
@@ -31566,7 +31656,7 @@ TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) {
   target.listen();
 
   Client cli("public.example", target.port());
-  cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
+  cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}});
   cli.set_proxy("127.0.0.1", proxy_mock.port());
   cli.set_keep_alive(true);
 
@@ -31898,6 +31988,141 @@ TEST(ProxyTunnelTest, OriginReturning407InsideTunnelDoesNotLeakProxyDigest) {
   EXPECT_FALSE(origin_saw_proxy_authz.load());
   EXPECT_EQ(1, proxy.connect_hits());
 }
+
+TEST(ProxyTunnelTest, ConnectTargetFollowsTunnelHostnameAddrMap) {
+  std::string origin_sni;
+  std::string origin_host;
+
+  proxy_tunnel_test::ScopedSSLServer origin;
+  origin.svr().Get(".*", [&](const Request &req, Response &res) {
+    origin_sni = req.sni();
+    origin_host = req.get_header_value("Host");
+    res.set_content("ok", "text/plain");
+  });
+  origin.listen();
+
+  proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
+  ASSERT_NE(0, proxy.port());
+
+  SSLClient cli("origin.example", origin.port());
+  cli.enable_server_certificate_verification(false);
+  cli.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}});
+  cli.set_proxy("127.0.0.1", proxy.port());
+
+  auto res = cli.Get("/x");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+
+  auto port = std::to_string(origin.port());
+  auto target = "127.0.0.1:" + port;
+  auto connect_req = proxy.connect_request();
+  EXPECT_EQ(1, proxy.connect_hits());
+  EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
+  EXPECT_NE(std::string::npos,
+            connect_req.find("\r\nHost: " + target + "\r\n"));
+  EXPECT_EQ("origin.example", origin_sni);
+  EXPECT_EQ("origin.example:" + port, origin_host);
+}
+
+TEST(ProxyTunnelTest, DigestRetryConnectTargetFollowsTunnelHostnameAddrMap) {
+  proxy_tunnel_test::ScopedSSLServer origin;
+  origin.svr().Get(".*", [&](const Request &, Response &res) {
+    res.set_content("ok", "text/plain");
+  });
+  origin.listen();
+
+  proxy_tunnel_test::ScopedConnectProxy proxy(
+      origin.port(), [](const std::string &head) -> std::string {
+        if (head.find(
+                "\r\nProxy-Authorization: Digest username=\"proxy-user\"") !=
+            std::string::npos) {
+          return proxy_tunnel_test::kTunnelEstablished;
+        }
+        return "HTTP/1.1 407 Proxy Authentication Required\r\n"
+               "Proxy-Authenticate: Digest realm=\"proxy\", qop=\"auth\", "
+               "nonce=\"abc\", algorithm=MD5\r\n"
+               "Content-Length: 0\r\n\r\n";
+      });
+  ASSERT_NE(0, proxy.port());
+
+  SSLClient cli("origin.example", origin.port());
+  cli.enable_server_certificate_verification(false);
+  cli.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}});
+  cli.set_proxy("127.0.0.1", proxy.port());
+  cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
+
+  auto res = cli.Get("/x");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+
+  // The retried CONNECT is the one the proxy saw last.
+  auto target = "127.0.0.1:" + std::to_string(origin.port());
+  auto connect_req = proxy.connect_request();
+  EXPECT_EQ(2, proxy.connect_hits());
+  EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
+  EXPECT_NE(std::string::npos,
+            connect_req.find("\r\nHost: " + target + "\r\n"));
+}
+
+TEST(ProxyTunnelTest, DirectHostnameAddrMapLeavesConnectTarget) {
+  proxy_tunnel_test::ScopedSSLServer origin;
+  origin.svr().Get(".*", [&](const Request &, Response &res) {
+    res.set_content("ok", "text/plain");
+  });
+  origin.listen();
+
+  proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
+  ASSERT_NE(0, proxy.port());
+
+  SSLClient cli("origin.example", origin.port());
+  cli.enable_server_certificate_verification(false);
+  cli.set_direct_hostname_addr_map({{"origin.example", "127.0.0.1"}});
+  cli.set_proxy("127.0.0.1", proxy.port());
+
+  auto res = cli.Get("/x");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+
+  auto target = "origin.example:" + std::to_string(origin.port());
+  auto connect_req = proxy.connect_request();
+  EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
+  EXPECT_NE(std::string::npos,
+            connect_req.find("\r\nHost: " + target + "\r\n"));
+}
+
+TEST(ProxyTunnelTest, RedirectKeepsTunnelHostnameAddrMap) {
+  proxy_tunnel_test::ScopedSSLServer origin;
+  auto port = std::to_string(origin.port());
+  origin.svr().Get("/x", [&](const Request &, Response &res) {
+    res.set_redirect("https://next.example:" + port + "/y");
+  });
+  origin.svr().Get("/y", [](const Request &, Response &res) {
+    res.set_content("ok", "text/plain");
+  });
+  origin.listen();
+
+  proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
+  ASSERT_NE(0, proxy.port());
+
+  SSLClient cli("origin.example", origin.port());
+  cli.enable_server_certificate_verification(false);
+  cli.set_tunnel_hostname_addr_map(
+      {{"origin.example", "127.0.0.1"}, {"next.example", "127.0.0.1"}});
+  cli.set_proxy("127.0.0.1", proxy.port());
+  cli.set_follow_location(true);
+
+  auto res = cli.Get("/x");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ("ok", res->body);
+
+  // The last CONNECT is the redirect client's.
+  ASSERT_EQ(2, proxy.connect_hits());
+  auto target = "127.0.0.1:" + port;
+  auto connect_req = proxy.connect_request();
+  EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
+  EXPECT_NE(std::string::npos,
+            connect_req.find("\r\nHost: " + target + "\r\n"));
+}
 #endif
 
 namespace proxy_tunnel_test {
@@ -32089,6 +32314,50 @@ TEST(WebSocketProxyTest, ProxySettingAppliesOnReconnect) {
   EXPECT_EQ(2, proxy.connect_hits());
 }
 
+TEST(WebSocketProxyTest, ConnectTargetFollowsTunnelHostnameAddrMap) {
+  proxy_tunnel_test::ScopedWebSocketOrigin origin;
+  proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
+  ASSERT_NE(0, proxy.port());
+
+  auto port = std::to_string(origin.port());
+  ws::WebSocketClient client("ws://origin.example:" + port + "/ws");
+  client.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}});
+  client.set_proxy("127.0.0.1", proxy.port());
+  auto res = client.connect();
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+
+  std::string msg;
+  ASSERT_TRUE(client.read(msg));
+  client.close();
+
+  EXPECT_EQ(1, proxy.connect_hits());
+  auto target = "127.0.0.1:" + port;
+  auto connect_req = proxy.connect_request();
+  EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
+  EXPECT_NE(std::string::npos,
+            connect_req.find("\r\nHost: " + target + "\r\n"));
+}
+
+TEST(WebSocketProxyTest, DirectHostnameAddrMapLeavesConnectTarget) {
+  proxy_tunnel_test::ScopedWebSocketOrigin origin;
+  proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
+  ASSERT_NE(0, proxy.port());
+
+  auto port = std::to_string(origin.port());
+  ws::WebSocketClient client("ws://origin.example:" + port + "/ws");
+  client.set_direct_hostname_addr_map({{"origin.example", "127.0.0.1"}});
+  client.set_proxy("127.0.0.1", proxy.port());
+  auto res = client.connect();
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  client.close();
+
+  auto target = "origin.example:" + port;
+  auto connect_req = proxy.connect_request();
+  EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
+  EXPECT_NE(std::string::npos,
+            connect_req.find("\r\nHost: " + target + "\r\n"));
+}
+
 #ifdef CPPHTTPLIB_SSL_ENABLED
 TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) {
   proxy_tunnel_test::ScopedWebSocketOrigin origin;