From 6c0f4cb9204e7de866191971dabe0f4e80ddddc6 Mon Sep 17 00:00:00 2001 From: gj Date: Fri, 24 Jul 2026 17:42:05 +0800 Subject: [PATCH] feat(cli): support IPv4/IPv6 dual-stack for admin_listen admin_listen currently only accepts a single {ip, port} object. Passing an array is silently ignored because the CLI reads only the .ip/.port scalars and falls back to the default 0.0.0.0:9180, so the Admin API can never listen on both IPv4 and IPv6 at once. Allow admin_listen to be either the existing object or an array of {ip, port}. The CLI collects every address and the nginx template emits one "listen" directive per address. The port conflict check now permits the same port to be reused across addresses of the same service, which is required for dual-stack. The single-object form keeps working unchanged (backward compatible), and config.yaml.example documents the new syntax. Test: extend t/cli/test_admin.sh to assert that the array form generates both "listen 0.0.0.0:9180;" and "listen [::]:9180;". Resolves #13744 Related: #7972 (earlier IPv6-on-control-plane request, closed as not planned without a fix) Signed-off-by: gj --- apisix/cli/ngx_tpl.lua | 4 ++++ apisix/cli/ops.lua | 28 +++++++++++++++++++--------- apisix/cli/schema.lua | 26 ++++++++++++++++++++++---- conf/config.yaml.example | 7 +++++++ t/cli/test_admin.sh | 30 ++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 4fa56d57a4cc..5b5b57f2efc2 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -616,7 +616,9 @@ http { {% if enable_admin then %} server { {%if https_admin then%} + {% for _, admin_server_addr in ipairs(admin_server_addrs) do %} listen {* admin_server_addr *} ssl; + {% end %} ssl_certificate {* admin_api_mtls.admin_ssl_cert *}; ssl_certificate_key {* admin_api_mtls.admin_ssl_cert_key *}; @@ -636,7 +638,9 @@ http { {% end %} {% else %} + {% for _, admin_server_addr in ipairs(admin_server_addrs) do %} listen {* admin_server_addr *}; + {% end %} {%end%} log_not_found off; diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 0f6a4fae6a00..87eed977bf8c 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -426,7 +426,9 @@ Please modify "admin_key" in conf/config.yaml . default_port, configured_port) local ip = configured_ip or default_ip local port = tonumber(configured_port) or default_port - if ports_to_check[port] ~= nil then + -- allow the same service (e.g. admin) to reuse one port across multiple + -- listen addresses, which is required by IPv4 + IPv6 dual-stack listening + if ports_to_check[port] ~= nil and ports_to_check[port] ~= port_name then util.die(port_name .. " ", port, " conflicts with ", ports_to_check[port], "\n") end ports_to_check[port] = port_name @@ -434,12 +436,20 @@ Please modify "admin_key" in conf/config.yaml . end -- listen in admin use a separate port, support specific IP, compatible with the original style - local admin_server_addr + -- admin_listen supports both a single {ip=, port=} object and an array of them. + -- The array form allows listening on multiple addresses, e.g. IPv4 + IPv6 dual-stack. + local admin_server_addrs = {} if yaml_conf.apisix.enable_admin then - local ip = yaml_conf.deployment.admin.admin_listen.ip - local port = yaml_conf.deployment.admin.admin_listen.port - admin_server_addr = validate_and_get_listen_addr("admin port", "0.0.0.0", ip, - 9180, port) + local admin_listen = yaml_conf.deployment.admin.admin_listen + if admin_listen[1] ~= nil then + for _, listen in ipairs(admin_listen) do + table_insert(admin_server_addrs, validate_and_get_listen_addr( + "admin port", "0.0.0.0", listen.ip, 9180, listen.port)) + end + else + table_insert(admin_server_addrs, validate_and_get_listen_addr( + "admin port", "0.0.0.0", admin_listen.ip, 9180, admin_listen.port)) + end end local status_server_addr @@ -701,9 +711,9 @@ Please modify "admin_key" in conf/config.yaml . local role = yaml_conf.deployment.role env.deployment_role = role - if role == "control_plane" and not admin_server_addr then + if role == "control_plane" and #admin_server_addrs == 0 then local listen = node_listen[1] - admin_server_addr = str_format("%s:%s", listen.ip, listen.port) + table_insert(admin_server_addrs, str_format("%s:%s", listen.ip, listen.port)) end end @@ -728,7 +738,7 @@ Please modify "admin_key" in conf/config.yaml . enabled_stream_plugins = enabled_stream_plugins, dubbo_upstream_multiplex_count = dubbo_upstream_multiplex_count, status_server_addr = status_server_addr, - admin_server_addr = admin_server_addr, + admin_server_addrs = admin_server_addrs, control_server_addr = control_server_addr, prometheus_server_addr = prometheus_server_addr, proxy_mirror_timeouts = proxy_mirror_timeouts, diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua index 3e3e855106c8..90673435feea 100644 --- a/apisix/cli/schema.lua +++ b/apisix/cli/schema.lua @@ -381,12 +381,30 @@ local admin_schema = { } }, admin_listen = { - properties = { - listen = { type = "string" }, - port = { type = "integer" }, + -- compatible with both the single-address object form and the + -- multi-address array form (IPv4 + IPv6 dual-stack) + oneOf = { + { + type = "object", + properties = { + ip = { type = "string" }, + port = { type = "integer" }, + }, + }, + { + type = "array", + minItems = 1, + items = { + type = "object", + properties = { + ip = { type = "string" }, + port = { type = "integer" }, + }, + }, + }, }, default = { - listen = "0.0.0.0", + ip = "0.0.0.0", port = 9180, } }, diff --git a/conf/config.yaml.example b/conf/config.yaml.example index d6714427ad4f..97828fc228fc 100644 --- a/conf/config.yaml.example +++ b/conf/config.yaml.example @@ -766,6 +766,13 @@ deployment: # Deployment configurations admin_listen: # Set the Admin API listening addresses. ip: 0.0.0.0 # Set listening IP. port: 9180 # Set listening port. Beware of port conflict with node_listen. + # admin_listen also accepts an array to listen on multiple addresses at once, + # for example IPv4 + IPv6 dual-stack (the same port may be reused across them): + # admin_listen: + # - ip: 0.0.0.0 + # port: 9180 + # - ip: "[::]" + # port: 9180 # https_admin: true # Enable SSL for Admin API on IP and port specified in admin_listen. # Use admin_api_mtls.admin_ssl_cert and admin_api_mtls.admin_ssl_cert_key. diff --git a/t/cli/test_admin.sh b/t/cli/test_admin.sh index 1298cc1dd406..613f5a990a8f 100755 --- a/t/cli/test_admin.sh +++ b/t/cli/test_admin.sh @@ -56,6 +56,36 @@ make stop echo "passed: admin https enabled" +# check admin_listen supports an array of addresses (IPv4 + IPv6 dual-stack) + +git checkout conf/config.yaml + +echo ' +apisix: + enable_admin: true +deployment: + admin: + admin_listen: + - ip: 0.0.0.0 + port: 9180 + - ip: "[::]" + port: 9180 +' > conf/config.yaml + +make init + +if ! grep "listen 0.0.0.0:9180;" conf/nginx.conf > /dev/null; then + echo "failed: admin_listen array should generate the IPv4 listen directive" + exit 1 +fi + +if ! grep "listen \[::\]:9180;" conf/nginx.conf > /dev/null; then + echo "failed: admin_listen array should generate the IPv6 listen directive" + exit 1 +fi + +echo "passed: admin_listen supports IPv4 + IPv6 dual-stack" + echo ' apisix: enable_admin: true