From a71db21d35c3ab1d03ce69ef50a49628e042b8d8 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Wed, 3 Dec 2025 20:36:47 +0000 Subject: [PATCH 1/2] add host_override to parent.config and other sni name fixes --- include/proxy/ParentSelection.h | 15 ++ .../http/remap/NextHopSelectionStrategy.h | 3 - src/proxy/ParentSelection.cc | 7 + src/proxy/http/HttpSM.cc | 6 +- src/proxy/http/HttpTransact.cc | 73 +++++++- src/proxy/http/remap/NextHopConsistentHash.cc | 4 - src/proxy/http/remap/NextHopRoundRobin.cc | 1 - .../http/remap/NextHopSelectionStrategy.cc | 10 -- tests/gold_tests/tls/ssl/gen_foobar_certs.sh | 29 ++++ tests/gold_tests/tls/ssl/server-bar.key | 28 +++ tests/gold_tests/tls/ssl/server-bar.pem | 22 +++ tests/gold_tests/tls/ssl/server-foo.key | 28 +++ tests/gold_tests/tls/ssl/server-foo.pem | 22 +++ .../tls/tls_sni_parent_failover.test.py | 159 ++++++++++++++++++ 14 files changed, 378 insertions(+), 29 deletions(-) create mode 100755 tests/gold_tests/tls/ssl/gen_foobar_certs.sh create mode 100644 tests/gold_tests/tls/ssl/server-bar.key create mode 100644 tests/gold_tests/tls/ssl/server-bar.pem create mode 100644 tests/gold_tests/tls/ssl/server-foo.key create mode 100644 tests/gold_tests/tls/ssl/server-foo.pem create mode 100644 tests/gold_tests/tls/tls_sni_parent_failover.test.py diff --git a/include/proxy/ParentSelection.h b/include/proxy/ParentSelection.h index 5960cfda189..8bfd556fcbe 100644 --- a/include/proxy/ParentSelection.h +++ b/include/proxy/ParentSelection.h @@ -167,6 +167,7 @@ class ParentRecord : public ControlBase int max_unavailable_server_retries = 1; int secondary_mode = 1; bool ignore_self_detect = false; + bool host_override = false; ParentHashAlgorithm consistent_hash_algorithm = ParentHashAlgorithm::SIPHASH24; uint64_t consistent_hash_seed0 = 0; uint64_t consistent_hash_seed1 = 0; @@ -242,6 +243,20 @@ struct ParentResult { return is_api_result() ? ParentRetry_t::NONE : rec->parent_retry; } + bool + host_override() const + { + if (is_api_result()) { + return false; + } + + if (!is_some()) { + return false; + } + + return rec->host_override; + } + unsigned max_retries(ParentRetry_t method) const { diff --git a/include/proxy/http/remap/NextHopSelectionStrategy.h b/include/proxy/http/remap/NextHopSelectionStrategy.h index 25e62676157..0623a9b4408 100644 --- a/include/proxy/http/remap/NextHopSelectionStrategy.h +++ b/include/proxy/http/remap/NextHopSelectionStrategy.h @@ -27,7 +27,6 @@ #include #include "ts/apidefs.h" -#include "proxy/ParentSelection.h" #include "proxy/http/HttpTransact.h" #ifndef _NH_UNIT_TESTS_ @@ -199,8 +198,6 @@ class NextHopSelectionStrategy const time_t now = 0); bool nextHopExists(TSHttpTxn txnp, void *ih = nullptr); - void setHostHeader(TSHttpTxn txnp, const char *hostname); - virtual ParentRetry_t responseIsRetryable(int64_t sm_id, HttpTransact::CurrentInfo ¤t_info, HTTPStatus response_code); void retryComplete(TSHttpTxn txn, const char *hostname, const int port); diff --git a/src/proxy/ParentSelection.cc b/src/proxy/ParentSelection.cc index 0870bcb9bf1..b282fba7b38 100644 --- a/src/proxy/ParentSelection.cc +++ b/src/proxy/ParentSelection.cc @@ -797,6 +797,13 @@ ParentRecord::Init(matcher_line *line_info) ignore_self_detect = false; } used = true; + } else if (strcasecmp(label, "host_override") == 0) { + if (strcasecmp(val, "true") == 0) { + host_override = true; + } else { + host_override = false; + } + used = true; } else if (strcasecmp(label, "hash_algorithm") == 0) { consistent_hash_algorithm = parseHashAlgorithm(val); used = true; diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index d9cfb662923..93da910620a 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -5274,11 +5274,11 @@ HttpSM::get_outbound_sni() const // By default the host header field value is used for the SNI. zret = t_state.hdr_info.server_request.host_get(); } else if (_ua.get_txn() && policy == "server_name"_tv) { - const char *server_name = snis->get_sni_server_name(); - if (server_name[0] == '\0') { + const char *const server_name = snis->get_sni_server_name(); + if (nullptr == server_name || server_name[0] == '\0') { zret.assign(nullptr, swoc::TextView::npos); } else { - zret.assign(snis->get_sni_server_name(), swoc::TextView::npos); + zret.assign(server_name, swoc::TextView::npos); } } else if (policy.front() == '@') { // guaranteed non-empty from previous clause zret = policy.remove_prefix(1); diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc index 0f5e689222d..662fd705cad 100644 --- a/src/proxy/http/HttpTransact.cc +++ b/src/proxy/http/HttpTransact.cc @@ -139,19 +139,51 @@ bypass_ok(HttpTransact::State *s) return false; } +// wrapper to choose between a remap next hop strategy or use parent.config +// remap next hop strategy is preferred +inline static bool +host_override(HttpTransact::State *s) +{ + if (s->response_action.handled) { // should be handled by the plugin + return false; + } else if (nullptr != s->next_hop_strategy) { + // remap strategies do not support the TSHttpTxnParentProxySet API. + return s->next_hop_strategy->host_override; + } else if (nullptr != s->parent_params) { + return s->parent_result.host_override(); + } + return false; +} + +// wrapper to choose between a remap next hop strategy or use parent.config +// remap next hop strategy is preferred +inline static bool +is_some(HttpTransact::State *s) +{ + if (s->response_action.handled) { + return true; + } else if (nullptr != s->next_hop_strategy) { + // remap strategies do not support the TSHttpTxnParentProxySet API. + return s->parent_result.result == ParentResultType::SPECIFIED; + } else if (nullptr != s->parent_params) { + return s->parent_result.is_some(); + } + return false; +} + // wrapper to choose between a remap next hop strategy or use parent.config // remap next hop strategy is preferred inline static bool is_api_result(HttpTransact::State *s) { - bool r = false; + bool res = false; if (nullptr != s->next_hop_strategy) { // remap strategies do not support the TSHttpTxnParentProxySet API. - r = false; + res = false; } else if (nullptr != s->parent_params) { - r = s->parent_result.is_api_result(); + res = s->parent_result.is_api_result(); } - return r; + return res; } // wrapper to get the max_retries. @@ -207,6 +239,8 @@ retry_type(HttpTransact::State *s) inline static void findParent(HttpTransact::State *s) { + TxnDbg(dbg_ctl_http_trans, "findParent"); + Metrics::Counter::increment(http_rsb.parent_count); if (s->response_action.handled) { s->parent_result.hostname = s->response_action.action.hostname; @@ -295,6 +329,8 @@ parentExists(HttpTransact::State *s) inline static void nextParent(HttpTransact::State *s) { + TxnDbg(dbg_ctl_http_trans, "nextParent"); + TxnDbg(dbg_ctl_parent_down, "connection to parent %s failed, conn_state: %s, request to origin: %s", s->parent_result.hostname, HttpDebugNames::get_server_state_name(s->current.state), s->request_data.get_host()); Metrics::Counter::increment(http_rsb.parent_count); @@ -599,10 +635,11 @@ find_server_and_update_current_info(HttpTransact::State *s) } else { findParent(s); } - if (!s->parent_result.is_some() || is_api_result(s) || parent_is_proxy(s)) { + if (!is_some(s) || is_api_result(s) || parent_is_proxy(s)) { TxnDbg(dbg_ctl_http_trans, "request not cacheable, so bypass parent"); s->parent_result.result = ParentResultType::DIRECT; } + } else if (s->txn_conf->uncacheable_requests_bypass_parent && s->txn_conf->no_dns_forward_to_parent == 0 && !HttpTransact::is_request_cache_lookupable(s)) { // request not lookupable and cacheable, so bypass parent if the parent is not an origin server. @@ -616,10 +653,12 @@ find_server_and_update_current_info(HttpTransact::State *s) } else { findParent(s); } - if (!s->parent_result.is_some() || is_api_result(s) || parent_is_proxy(s)) { + + if (!is_some(s) || is_api_result(s) || parent_is_proxy(s)) { TxnDbg(dbg_ctl_http_trans, "request not cacheable, so bypass parent"); s->parent_result.result = ParentResultType::DIRECT; } + } else { switch (s->parent_result.result) { case ParentResultType::UNDEFINED: @@ -660,14 +699,32 @@ find_server_and_update_current_info(HttpTransact::State *s) } switch (s->parent_result.result) { - case ParentResultType::SPECIFIED: - s->parent_info.name = s->arena.str_store(s->parent_result.hostname, strlen(s->parent_result.hostname)); + case ParentResultType::SPECIFIED: { + char const *const hostname = s->parent_result.hostname; + + if (nullptr != hostname) { + s->parent_info.name = s->arena.str_store(hostname, strlen(hostname)); + + // if host header override option enabled + if (host_override(s)) { + TxnDbg(dbg_ctl_http_trans, "overriding host header with parent %s", hostname); + if (!s->hdr_info.server_request.valid()) { + s->hdr_info.client_request.value_set(static_cast(MIME_FIELD_HOST), hostname); + s->hdr_info.client_request.mark_target_dirty(); + } else { + s->hdr_info.server_request.value_set(static_cast(MIME_FIELD_HOST), hostname); + s->hdr_info.server_request.mark_target_dirty(); + } + } + } + update_current_info(&s->current, &s->parent_info, ResolveInfo::PARENT_PROXY, false); update_dns_info(&s->dns_info, &s->current); ink_assert(s->dns_info.looking_up == ResolveInfo::PARENT_PROXY); s->next_hop_scheme = URL_WKSIDX_HTTP; return ResolveInfo::PARENT_PROXY; + } case ParentResultType::FAIL: // No more parents - need to return an error message s->current.request_to = ResolveInfo::HOST_NONE; diff --git a/src/proxy/http/remap/NextHopConsistentHash.cc b/src/proxy/http/remap/NextHopConsistentHash.cc index 848e78a751f..635e153c1e6 100644 --- a/src/proxy/http/remap/NextHopConsistentHash.cc +++ b/src/proxy/http/remap/NextHopConsistentHash.cc @@ -547,8 +547,4 @@ NextHopConsistentHash::findNextHop(TSHttpTxn txnp, void * /* ih ATS_UNUSED */, t NH_Dbg(NH_DBG_CTL, "[%" PRIu64 "] result.result: %s set hostname null port 0 retry false", sm_id, ParentResultStr[static_cast(result.result)]); } - - setHostHeader(txnp, result.hostname); - - return; } diff --git a/src/proxy/http/remap/NextHopRoundRobin.cc b/src/proxy/http/remap/NextHopRoundRobin.cc index 3acb8411c32..c04acbdaa50 100644 --- a/src/proxy/http/remap/NextHopRoundRobin.cc +++ b/src/proxy/http/remap/NextHopRoundRobin.cc @@ -170,7 +170,6 @@ NextHopRoundRobin::findNextHop(TSHttpTxn txnp, void * /* ih ATS_UNUSED */, time_ result->last_parent = cur_hst_index; result->last_group = cur_grp_index; result->retry = parentRetry; - setHostHeader(txnp, result->hostname); ink_assert(result->hostname != nullptr); ink_assert(result->port != 0); NH_Dbg(NH_DBG_CTL, "[%" PRIu64 "] Chosen parent = %s.%d", sm_id, result->hostname, result->port); diff --git a/src/proxy/http/remap/NextHopSelectionStrategy.cc b/src/proxy/http/remap/NextHopSelectionStrategy.cc index e308e73a4bb..6ca4cf78ef6 100644 --- a/src/proxy/http/remap/NextHopSelectionStrategy.cc +++ b/src/proxy/http/remap/NextHopSelectionStrategy.cc @@ -265,16 +265,6 @@ NextHopSelectionStrategy::markNextHop(TSHttpTxn txnp, const char *hostname, cons return passive_health.markNextHop(txnp, hostname, port, status, ih, now); } -void -NextHopSelectionStrategy::setHostHeader(TSHttpTxn txnp, const char *hostname) -{ - if (host_override && nullptr != hostname) { - HttpSM *sm = reinterpret_cast(txnp); - sm->t_state.hdr_info.client_request.value_set(static_cast(MIME_FIELD_HOST), hostname); - NH_Dbg(NH_DBG_CTL, "[%" PRIu64 "] overriding host header with parent %s", sm->sm_id, hostname); - } -} - bool NextHopSelectionStrategy::nextHopExists(TSHttpTxn txnp, void * /* ih ATS_UNUSED */) { diff --git a/tests/gold_tests/tls/ssl/gen_foobar_certs.sh b/tests/gold_tests/tls/ssl/gen_foobar_certs.sh new file mode 100755 index 00000000000..ad45e8d8364 --- /dev/null +++ b/tests/gold_tests/tls/ssl/gen_foobar_certs.sh @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# bash script used to generate sni parent test certs + +for name in foo bar +do + openssl req -x509 -newkey rsa:2048 \ + -keyout server-${name}.key -out server-${name}.pem \ + -days 3650 -nodes \ + -subj "/C=US/ST=CO/L=Denver/O=Comcast/OU=Edge/CN=${name}.com" \ + -addext "subjectAltName=DNS:${name}.com,DNS:www.${name}.com,DNS:*.${name}.com" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -addext "keyUsage=critical,keyCertSign,cRLSign" \ + -addext "subjectKeyIdentifier=hash" +done diff --git a/tests/gold_tests/tls/ssl/server-bar.key b/tests/gold_tests/tls/ssl/server-bar.key new file mode 100644 index 00000000000..afe8e73254b --- /dev/null +++ b/tests/gold_tests/tls/ssl/server-bar.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7hSSKR+PDyaK9 +Qf5Wi2DoBedwTaIiHRVUOmjGMhb9bHH/xQ1xXbIlXhl9LB5z6KWZTLN/6tDbJMlk +SF5CEPLXt0+XSwpx1ZZpBaxInKCZoiUbfazwb95y8evPYfOOmMR8tP/BKtygo0+U +MbzjpIGZEdOkyWF6U4BtMJopC41JOxkrwvGG3DDBDNMui3qAgpmedylGhz7Lilpu +XLc2cprtIBbo8jcQj4G0My5flZhvHmFe29L2YboLG2JtS8e39zOe+pfLb7T0JmDm +UgxeJB7KH/d7BI90J1tYctVH2ky20oXQ2R1IC2bhsfuDEIo5OhPu56rINSmvXYDD +RHL1nJH9AgMBAAECggEAJYzSVdxyeBzS+UoTR7pOU/gGsd7h5injwQOXQoT6RJIo +O9r2R6rHByOSQBYLHRPLwWhyE13up8t0hp/VPC6PqnG7PuUYeNYX8fzgVIQExu8I +tjoS3OzR4zudiGApePcPdZd7FW/jUUmSoG7bT8x75S6ELp3i5q986qCOZkS5bJ3Q +Tr6LdgwYsdRMuwTBmHFym3obd8rVe1oQ4QY2M0mYAraGnFxiCNV+S+hDNsJ7na3F +hbeBk+YpFKZ/dGDrKClhNbOmpSZmazihHaYoKrBc+fhJSzxVU5YfN+Ds7jsOptpa +m0pr8Di3QnibkIUrjNiB3ewkIi+tyo0YO5Zzq406oQKBgQDxt4r/6hni0mBY3VrR +UT74gzGihqxfXTLjq7Hiju9BC5qK9MaVRl+s5I6+phfSg4kyJSFsoWxcoKYFE/Km +4xpk3sJlVIBwRBmx76h5B3eEpLayhoBR2TT0Cl5M8yhvz2p8CQO/MmGLP3qe/PqH +32rBNLAs65e4ts2uKUJMC+AkrQKBgQDGmcM6vgRtq/DASFMMqpimSX+8fj9AGqLf +mVcItXHx7pS1Aiq1G3PSA4efSryMjoleez3mfqFoPvTSP0ASIaQua9rAerfWfYaG +/P8yAG7oddBb0SDe7f05VjY5jv3dS7sSDHpfaIa06xtRxd6HqUo7iPOir0FR6UzA +iNkxKfR8kQKBgQCnhxxxjmDukfxw2soM9RB90P/fsxNY7RFONjuN2J7+J/qugEP6 +RdId1DMS867jGoNGG/H0hlTCRh2Ku26cOB6c9r8o185FAQ4GAyJy77foWPi+9vWM +xMwsr9r33jeSduFIoj7UjyiICDEGbDN/ZFtrGQdZutdnEFuxb6shZcGt6QKBgFJj +r65K7iNhVTsvxeRXUYSKsUdNSIgbhL4mKwkd3Ot1ApQlFfqULPRPKpBWvOnCqBJe +Jkvc9LD+jSo7uyTKeAYaEGIRhvqgkJKnmmbv7xLY7Vtp4q0ZJhgHP++Y9pA7vpu6 +OXojLt8XOfoukCbPgFA6fHhdJEgK9SBapV/T++1BAoGBAMzO2jugm25fBzqWQpSe +JV/CoQIbw2SUyXunsReIMqnYBETUDvzPrvQ0aRWUPtkzwf+vlbU7vauX4CRogB8e +PWakmkCuSjPlxqycYUk/xIYEuaXn7Kk7IeF3UEbeSJLq9+cXeDun8OSE9lmD2Phn +ITIVjf8kn1qEPgfr9GlGnaSN +-----END PRIVATE KEY----- diff --git a/tests/gold_tests/tls/ssl/server-bar.pem b/tests/gold_tests/tls/ssl/server-bar.pem new file mode 100644 index 00000000000..1a22b5935a9 --- /dev/null +++ b/tests/gold_tests/tls/ssl/server-bar.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnTCCAoWgAwIBAgIUS33d4I/Zq7YeC+bDnGr/e1BJkdwwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMQ8wDQYDVQQHDAZEZW52ZXIx +EDAOBgNVBAoMB0NvbWNhc3QxDTALBgNVBAsMBEVkZ2UxEDAOBgNVBAMMB2Jhci5j +b20wHhcNMjUxMjE3MTcxMDA0WhcNMzUxMjE1MTcxMDA0WjBeMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ08xDzANBgNVBAcMBkRlbnZlcjEQMA4GA1UECgwHQ29tY2Fz +dDENMAsGA1UECwwERWRnZTEQMA4GA1UEAwwHYmFyLmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALuFJIpH48PJor1B/laLYOgF53BNoiIdFVQ6aMYy +Fv1scf/FDXFdsiVeGX0sHnPopZlMs3/q0NskyWRIXkIQ8te3T5dLCnHVlmkFrEic +oJmiJRt9rPBv3nLx689h846YxHy0/8Eq3KCjT5QxvOOkgZkR06TJYXpTgG0wmikL +jUk7GSvC8YbcMMEM0y6LeoCCmZ53KUaHPsuKWm5ctzZymu0gFujyNxCPgbQzLl+V +mG8eYV7b0vZhugsbYm1Lx7f3M576l8tvtPQmYOZSDF4kHsof93sEj3QnW1hy1Ufa +TLbShdDZHUgLZuGx+4MQijk6E+7nqsg1Ka9dgMNEcvWckf0CAwEAAaNTMFEwHQYD +VR0OBBYEFPSlsbODTwZv1B8DmEyvQSzKWKQTMB8GA1UdIwQYMBaAFPSlsbODTwZv +1B8DmEyvQSzKWKQTMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB +ACOxraNGdHERXHVCW3ICKd14ASA+Xewc2pQrjc/VEAuOH10G1yyDzHCNZrmiJFYN +IAje3JTfagKK6dFXgeC8tbJhHoNlC+Y/tLxdReuRshJdEraD/2ih2NgUqsI4soJT +EhB+pUIeni5hF+ymWzewx3axrnblDEt5nl50zOnKKdHuDO6JlovnI50JaFdduWAS +oOrornY+r6WO9CKt9uLmf/y4Epn6yFX7AL9aDzQQsS62+4o/nkQLf/u5eFRSLJK2 +Jj+QAWNP37is8q+2Jgmbt3DWybOq2znN1edNtUYjRCNafiK9qrhGeNp+yI/qB8dm +W5pOyGNb+j9iY39KbNkSmTM= +-----END CERTIFICATE----- diff --git a/tests/gold_tests/tls/ssl/server-foo.key b/tests/gold_tests/tls/ssl/server-foo.key new file mode 100644 index 00000000000..11d104b6e96 --- /dev/null +++ b/tests/gold_tests/tls/ssl/server-foo.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCHQOoMuJlA4tSU +DVSDcrb/ggBNvN4DIh3d4briYf2QFB1Gx7pnOwTG3Mt6jc6i/cBmOUhFWnYpe6An +0on3ARlDzdXj3BiAd1Iw+BXCjxSGW6fVEWwalg09FkgOhRasYwOxggI01fGuY0uH +CgRMi++4Xgr6umFFDh5hjvX1dLcn5gedseGLK+goiYI8ZPQIXWfIoKDAVb8aDFVu ++gIaZwexkQsSB/gr9uTqLNCZNlaI13fCuOFGLztvERDLwoQ88y1u9v9SpWnOB35l +bbXf9jdcnv67ug0N04gtAYIUgyOOB0THDsjH2v8gKY7mL5UEOjzM0ZIKHGVDRSAp +hgakHRVbAgMBAAECgf8h6GgJcWSImLm1168RQeQku9mcPYN75+izDBxMZvr67ZDX +0p8Rrv+Oc6Wen5J/d2mJU5ol6mWVa90gG1IOeFBOHpvTn8Ki4Lc4oFVuJtobtSoE +wKIwcy/r2YSFtEZRETZb1BZxkfNlS3cRdvMVbi6AVJrYzwkqTKHs01mgrf7zRs1G +dpqSj32j4q6H/C6jF+3hBGfWX2HphNzEknZCjrtTqFDIdZ+v9XIAmthfAysKWLII +oDcDj2NZbBgs32Woy90BTakxpE5r07spplXBdrEboF4ZikmZTd9GwMtY2pfzEMQ0 +IZzPyGyRyU8knr50mDeFNfSTcDdASUj5sBnYeQUCgYEAvQoDkGKm+DZNvI/ltTIF +1cgcnb62Lrp+GHfSGNV4V0n3uQxyWLpFItARom5nfwrWoyAo+hskbqmAl2OcJj9J +p3WIaudYb5NTzXYi4eY4gTOuyiXXsXR+PO1RvZEQnCPJAXH+Y+GxPVII5oxYcWeN +mAnl5UrD17efMB3f2E8wDd8CgYEAtymmbkrxHMTry0JqR+r47i4eRS4LvXyPpVDa +MMx8nHPqeIfYEgMjgmkBDZIHainKCoFsHseevLwTPz7UN4AXOqqYND4csZsjqAiI +2/PEFiPczWiJkWN1JoUVBHgTdvrSaIEZ4xYsBfthfzgqan8Tkt/tDI9ysrWdC/QP +EkbNMAUCgYA93bEc56xNPzhhpZY/nodaV4tF9Mwart3llBEmH3aq2oJABVrGvu8x +XXbn+cnVQe7MpBHFCGz76m0zHl5UBhJMw6JJmjzPByA99ZOk6ntDjW5+qbPVV5KY +zfIaYAdXkj6OQohGl+4xa0+OZA/tzT4Bq/uY9sbxKh7m2GARopjPcQKBgG1jboZI +M5+e68PEsnypFGoLGQDv8wsDrTZqByVFutYlVE8PsjdvsHFeDMbyA4Of2Y5UpSHs +zhyhpk0LVOqgkT70S1pIDhL0OGNOVY4nE2C1olT6rc4qu/h5WogEvns8aRUIpVE+ +GKSp8Rxtisd3hVUebxAlS7b3SAevhqC/sCORAoGAGy4yXxWrsNBu+OPpLDTiN3WN +oou1MfVWx/5VmW71w5EUW1tZ1Umva6OSRszO8niRMARYvrqXMoOYRjHEaB1LoTiz +berNI55iVxlfiwnl7AzigxU+EaDpf5wLPLi7yq5PJZSXW3TC5HZrqvVOEc4CAb1T +7p7zNQCKEL3QA2jUymw= +-----END PRIVATE KEY----- diff --git a/tests/gold_tests/tls/ssl/server-foo.pem b/tests/gold_tests/tls/ssl/server-foo.pem new file mode 100644 index 00000000000..ff9366f21b2 --- /dev/null +++ b/tests/gold_tests/tls/ssl/server-foo.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnTCCAoWgAwIBAgIUKNL6X8hvp7XV9J3DJg/IXI0XsawwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMQ8wDQYDVQQHDAZEZW52ZXIx +EDAOBgNVBAoMB0NvbWNhc3QxDTALBgNVBAsMBEVkZ2UxEDAOBgNVBAMMB2Zvby5j +b20wHhcNMjUxMjE3MTcwOTU1WhcNMzUxMjE1MTcwOTU1WjBeMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ08xDzANBgNVBAcMBkRlbnZlcjEQMA4GA1UECgwHQ29tY2Fz +dDENMAsGA1UECwwERWRnZTEQMA4GA1UEAwwHZm9vLmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAIdA6gy4mUDi1JQNVINytv+CAE283gMiHd3huuJh +/ZAUHUbHumc7BMbcy3qNzqL9wGY5SEVadil7oCfSifcBGUPN1ePcGIB3UjD4FcKP +FIZbp9URbBqWDT0WSA6FFqxjA7GCAjTV8a5jS4cKBEyL77heCvq6YUUOHmGO9fV0 +tyfmB52x4Ysr6CiJgjxk9AhdZ8igoMBVvxoMVW76AhpnB7GRCxIH+Cv25Oos0Jk2 +VojXd8K44UYvO28REMvChDzzLW72/1Klac4HfmVttd/2N1ye/ru6DQ3TiC0BghSD +I44HRMcOyMfa/yApjuYvlQQ6PMzRkgocZUNFICmGBqQdFVsCAwEAAaNTMFEwHQYD +VR0OBBYEFGKhV4jaRQ0wg1AWGgJleyIxP/UzMB8GA1UdIwQYMBaAFGKhV4jaRQ0w +g1AWGgJleyIxP/UzMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB +AEQ6IHWosM7rA0OTjTT8Cuovg1hO+EgFyXzzNYRUSNjlidXYPRfgXO1fRAK9Xzca +dO+meaMgLxWK5AOcrtOLkorJom1tHzMsJmneRGiONGmGSNN84Fk0RwVWKz9Aumoj +WVz+z/DEV+Myw8BQNP2BtZBX5MsDGWvLcZsZY9YNofIPvAVJONpiAuowc4/IQHH9 +156X3GYgaFkFEV9g6QqFpitR470DKihky8zpspJoeHD/pnZyY+3afKyU4i0YVPpa +ZHri+ITdWjbqaRMewbcckIB6I+wWXTImHNibcL8sd1yLpw5+IDaV7AdMQmLj5Tmw +6eiYbN3uwTKx79XwGOas1T0= +-----END CERTIFICATE----- diff --git a/tests/gold_tests/tls/tls_sni_parent_failover.test.py b/tests/gold_tests/tls/tls_sni_parent_failover.test.py new file mode 100644 index 00000000000..d1b0e0d87d2 --- /dev/null +++ b/tests/gold_tests/tls/tls_sni_parent_failover.test.py @@ -0,0 +1,159 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Test.Summary = ''' +Test parent failover with SNI name handling, validating that Traffic Server +correctly fails over between HTTPS parents (including strategy-based parents) +while enforcing TLS server name verification. +''' + +# Define default ATS +ts = Test.MakeATSProcess( + "ts", + enable_tls=True, + enable_cache=False, +) + +server_foo = Test.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/server-foo.key".format(Test.RunDirectory), + "--cert": "{0}/server-foo.pem".format(Test.RunDirectory), + }, +) +server_bar = Test.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/server-bar.key".format(Test.RunDirectory), + "--cert": "{0}/server-bar.pem".format(Test.RunDirectory), + }, +) + +# default check request/response +request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_foo_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "foo ok"} +request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_bar_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "bar ok"} + +server_foo.addResponse("sessionlog.json", request_foo_header, response_foo_header) +server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) + +# successful request to be served by bar.com +request_bar_header = {"headers": "GET /path HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_bar_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "path bar ok" +} + +server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) + +ts.addSSLfile("ssl/server-foo.pem") +ts.addSSLfile("ssl/server-foo.key") +ts.addSSLfile("ssl/server-bar.pem") +ts.addSSLfile("ssl/server-bar.key") + +dns = Test.MakeDNServer("dns") + +ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl|parent_select|next_hop', + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'NAME', + 'proxy.config.url_remap.pristine_host_hdr': 0, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.connect.down.policy': 1, # tls failures don't mark down + }) + +dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) +dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) +dns.addRecords(records={"parent.": ["127.0.0.1"]}) +dns.addRecords(records={"strategy.": ["127.0.0.1"]}) + +ts.Disk.remap_config.AddLines([ + "map http://parent https://parent", + "map http://strategy https://strategy @strategy=strat", +]) + +ts.Disk.parent_config.AddLine( + 'dest_domain=. port=443 parent="foo.com:{0}|1;bar.com:{1}|1" parent_retry=simple_retry parent_is_proxy=false go_direct=false simple_server_retry_responses="404" host_override=true' + .format(server_foo.Variables.SSL_Port, server_bar.Variables.SSL_Port)) + +# build strategies.yaml file +ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + +s = ts.Disk.strategies +s.AddLine("groups:") +s.AddLines( + [ + f" - &gstrat", + f" - host: foo.com", + f" protocol:", + f" - scheme: https", + f" port: {server_foo.Variables.SSL_Port}", + f" weight: 1.0", + f" - host: bar.com", + f" protocol:", + f" - scheme: https", + f" port: {server_bar.Variables.SSL_Port}", + f" weight: 1.0", + ]) + +s.AddLine("strategies:") + +s.AddLines( + [ + f" - strategy: strat", + f" policy: first_live", + f" go_direct: false", + f" parent_is_proxy: false", + f" ignore_self_detect: true", + f" host_override: true", + f" groups:", + f" - *gstrat", + f" scheme: https", + f" failover:", + f" ring_mode: exhaust_ring", + f" response_codes:", + f" - 404", + ]) + +curl_args = f"-s -L -o /dev/stdout -D /dev/stderr -x localhost:{ts.Variables.port} " + +tr = Test.AddTestRun("request with failover, parent.config") +tr.Setup.Copy("ssl/server-foo.key") +tr.Setup.Copy("ssl/server-foo.pem") +tr.Setup.Copy("ssl/server-bar.key") +tr.Setup.Copy("ssl/server-bar.pem") +tr.MakeCurlCommand(curl_args + "http://parent/path", ts=ts) +tr.StillRunningAfter = ts +ps = tr.Processes.Default +ps.StartBefore(server_foo) +ps.StartBefore(server_bar) +ps.StartBefore(dns) +ps.StartBefore(Test.Processes.ts) +ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") + +tr = Test.AddTestRun("request with failover, strategies.yaml") +tr.MakeCurlCommand(curl_args + "http://strategy/path", ts=ts) +tr.StillRunningAfter = ts +ps = tr.Processes.Default +ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") From 943fad1f9ba2150f91a501c31d422d5d982a7c2f Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 27 Feb 2026 13:47:06 +0000 Subject: [PATCH 2/2] add documentation for host_override in parent.config, update strategies.yaml docs --- doc/admin-guide/files/parent.config.en.rst | 8 ++++++++ doc/admin-guide/files/strategies.yaml.en.rst | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/admin-guide/files/parent.config.en.rst b/doc/admin-guide/files/parent.config.en.rst index 7b8fa4a56d3..64fa4d89749 100644 --- a/doc/admin-guide/files/parent.config.en.rst +++ b/doc/admin-guide/files/parent.config.en.rst @@ -324,6 +324,14 @@ The following list shows the possible actions and their allowed values. - ``false`` - The default. Do not ignore the host status. +.. _parent-config-format-host_override: + +``host_override`` + One of the following values: + + - ``true`` - Sets the host header of the request using the selected parent, including the SNI name to be used for any upstream TLS connection. Useful when a parent is another CDN that requires a correct SNI name. + - ``false`` - The default. Does not change the host header. + .. _parent-config-format-hash-algorithm: ``hash_algorithm`` diff --git a/doc/admin-guide/files/strategies.yaml.en.rst b/doc/admin-guide/files/strategies.yaml.en.rst index ec7c223c8a6..7e9852f8305 100644 --- a/doc/admin-guide/files/strategies.yaml.en.rst +++ b/doc/admin-guide/files/strategies.yaml.en.rst @@ -286,7 +286,7 @@ Each **strategy** in the list may using the following parameters: (**self** should only be necessary when the local hostname can only be translated to an IP address with a DNS lookup.) -- **host_override**: A boolean value that will set the host header to the selected parent rather than the original host. **true** sets host header to selected parent. **false** (default) leaves the host header untouched. +- **host_override**: A boolean value that will set the host header to the selected parent rather than the original host, including the SNI name for any attempted upstream TLS connection. **true** sets host header to selected parent. **false** (default) leaves the host header untouched. Example: ::