diff --git a/apisix/healthcheck_manager.lua b/apisix/healthcheck_manager.lua index 14d1a966d86d..1a69cf53245c 100644 --- a/apisix/healthcheck_manager.lua +++ b/apisix/healthcheck_manager.lua @@ -57,11 +57,15 @@ local function compute_targets(up_conf) local host = up_conf.checks and up_conf.checks.active and up_conf.checks.active.host local port = up_conf.checks and up_conf.checks.active and up_conf.checks.active.port local up_hdr = up_conf.pass_host == "rewrite" and up_conf.upstream_host - local use_node_hdr = up_conf.pass_host == "node" or nil local targets = {} for _, node in ipairs(up_conf.nodes) do - local host_hdr = up_hdr or (use_node_hdr and node.domain) or nil + -- A health check has no client; the gateway probes the node by the node's own + -- identity (its domain), so use node.domain as the Host header regardless of + -- pass_host (except "rewrite", which pins an explicit Host). Otherwise a domain + -- node probes with the resolved ip as Host/SNI, breaking HTTPS health checks. + -- node.domain is nil for ip nodes. + local host_hdr = up_hdr or node.domain local target_port = port or node.port -- add_target defaults the hostname to the ip when checks.active.host is -- unset, so mirror that here to match the shm entry's stored hostname diff --git a/t/node/healthcheck-node-domain-host.t b/t/node/healthcheck-node-domain-host.t new file mode 100644 index 000000000000..60fc1542520f --- /dev/null +++ b/t/node/healthcheck-node-domain-host.t @@ -0,0 +1,190 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + +repeat_each(1); +log_level('info'); +no_root_location(); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->http_config) { + # probe targets that log what the active health checker actually sends: + # the Host header over HTTP, and the TLS SNI over HTTPS. + my $http_config = <<'_EOC_'; +server { + listen 1988; + location /healthz { + content_by_lua_block { + ngx.log(ngx.WARN, "probe Host: ", ngx.var.http_host) + ngx.say("ok") + } + } +} + +server { + listen 1989 ssl; + ssl_certificate ../../certs/apisix.crt; + ssl_certificate_key ../../certs/apisix.key; + location /healthz { + content_by_lua_block { + ngx.log(ngx.WARN, "probe SNI: ", ngx.var.ssl_server_name) + ngx.say("ok") + } + } +} +_EOC_ + $block->set_value("http_config", $http_config); + } + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: domain node with the default pass_host is probed with the node domain as Host +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local original_parse_domain = core.resolver.parse_domain + core.resolver.parse_domain = function(domain) + if domain == "test.com" then + return "127.0.0.1", nil + end + return original_parse_domain(domain) + end + + local t = require("lib.test_admin").test + local code = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "type": "roundrobin", + "nodes": { "test.com:1988": 1 }, + "checks": { + "active": { + "http_path": "/healthz", + "healthy": { "interval": 1, "successes": 1 }, + "unhealthy": { "interval": 1, "http_failures": 1 } + } + } + }]] + ) + if code >= 300 then + ngx.status = code + return + end + + code = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ "uri": "/hello", "upstream_id": "1" }]] + ) + if code >= 300 then + ngx.status = code + return + end + + local http = require "resty.http" + local httpc = http.new() + httpc:request_uri("http://127.0.0.1:" .. ngx.var.server_port .. "/hello", + {method = "GET", keepalive = false}) + + ngx.sleep(2) + + core.resolver.parse_domain = original_parse_domain + ngx.say("done") + } + } +--- response_body +done +--- error_log +probe Host: test.com +--- no_error_log +probe Host: 127.0.0.1 +--- timeout: 5 + + + +=== TEST 2: over HTTPS, the domain node is probed with the node domain as SNI (not the resolved ip) +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local original_parse_domain = core.resolver.parse_domain + core.resolver.parse_domain = function(domain) + if domain == "test.com" then + return "127.0.0.1", nil + end + return original_parse_domain(domain) + end + + local t = require("lib.test_admin").test + local code = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "type": "roundrobin", + "scheme": "https", + "nodes": { "test.com:1989": 1 }, + "checks": { + "active": { + "type": "https", + "http_path": "/healthz", + "https_verify_certificate": false, + "healthy": { "interval": 1, "successes": 1 }, + "unhealthy": { "interval": 1, "http_failures": 1 } + } + } + }]] + ) + if code >= 300 then + ngx.status = code + return + end + + code = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ "uri": "/hello", "upstream_id": "1" }]] + ) + if code >= 300 then + ngx.status = code + return + end + + local http = require "resty.http" + local httpc = http.new() + httpc:request_uri("http://127.0.0.1:" .. ngx.var.server_port .. "/hello", + {method = "GET", keepalive = false}) + + ngx.sleep(2) + + core.resolver.parse_domain = original_parse_domain + ngx.say("done") + } + } +--- response_body +done +--- error_log +probe SNI: test.com +--- no_error_log +probe SNI: 127.0.0.1 +--- timeout: 5