From 086816c90d8b403b3a02e9210f5c79021b25ee2a Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 2 Jul 2026 18:51:13 +0900 Subject: [PATCH 1/2] [core]: select callback ip by target Select the management callback address by remote target IP version in dual-stack MN deployments. Keep explicit REST callback headers unchanged and keep non-IP targets on the existing default callback URL. Resolves: ZSTAC-86567 Change-Id: Id474bdc9002f25dfb1cf6334f0b18ffad3d2a526 --- .../main/java/org/zstack/core/Platform.java | 35 +++++++++++++++++ .../org/zstack/core/rest/RESTFacadeImpl.java | 29 +++++++++++++- .../ceph/backup/CephBackupStorageMonBase.java | 2 +- .../primary/CephPrimaryStorageMonBase.java | 2 +- .../src/main/java/org/zstack/kvm/KVMHost.java | 4 +- .../backup/sftp/SftpBackupStorage.java | 2 +- .../storage/zbs/ZbsPrimaryStorageMdsBase.java | 2 +- .../core/ManagementNetworkIpv6Case.groovy | 39 +++++++++++++++++++ 8 files changed, 108 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 67bdbb40a34..fa38efcb033 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1387,6 +1387,41 @@ public static String getRouteSourceIp(String remoteIp) { return null; } + public static String getManagementServerIpForRemote(String remoteIp) { + if (StringUtils.isBlank(remoteIp)) { + return getManagementServerIp(); + } + + remoteIp = normalizeManagementIp(remoteIp); + return selectManagementServerIpForRemote(remoteIp, getRouteSourceIp(remoteIp)); + } + + public static String selectManagementServerIpForRemote(String remoteIp, String routeSourceIp) { + if (StringUtils.isBlank(remoteIp)) { + return getManagementServerIp(); + } + + remoteIp = normalizeManagementIp(remoteIp); + routeSourceIp = normalizeManagementIp(routeSourceIp); + if (IPv6NetworkUtils.isIpv6Address(remoteIp)) { + if (IPv6NetworkUtils.isIpv6Address(routeSourceIp)) { + return routeSourceIp; + } + String ip6 = getManagementServerIp6(); + return ip6 == null ? getManagementServerIp() : ip6; + } + + if (NetworkUtils.isIpv4Address(remoteIp)) { + if (NetworkUtils.isIpv4Address(routeSourceIp)) { + return routeSourceIp; + } + String ip4 = getManagementServerIp4(); + return ip4 == null ? getManagementServerIp() : ip4; + } + + return getManagementServerIp(); + } + public static String selectManagementServerIp(Collection addresses) { String ipv4 = null; String ipv6 = null; diff --git a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java index 915a8ec3531..26dbb7ec675 100755 --- a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java @@ -50,12 +50,14 @@ import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; import org.zstack.utils.network.IPv6NetworkUtils; +import org.zstack.utils.network.NetworkUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -227,6 +229,31 @@ public static String buildCallbackUrl(String hostName, int port, String path) { return ub.build().toUriString(); } + public static String selectCallbackUrl(String requestUrl, Map headers, String defaultCallbackUrl, int port, String path) { + if (headers != null && headers.keySet().stream().anyMatch(RESTConstant.CALLBACK_URL::equalsIgnoreCase)) { + return defaultCallbackUrl; + } + + String host = extractRequestHost(requestUrl); + if (host == null) { + return defaultCallbackUrl; + } + + if (!NetworkUtils.isIpv4Address(host) && !IPv6NetworkUtils.isIpv6Address(host)) { + return defaultCallbackUrl; + } + + return buildCallbackUrl(Platform.getManagementServerIpForRemote(host), port, path); + } + + private static String extractRequestHost(String requestUrl) { + try { + return IPv6NetworkUtils.stripHostUrlBrackets(new URI(requestUrl).getHost()); + } catch (URISyntaxException | IllegalArgumentException e) { + return null; + } + } + public static String buildSendCommandUrl(String hostName, int port, String path) { UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(buildBaseUrl(hostName, port, path)); ub.path(RESTConstant.COMMAND_CHANNEL_PATH); @@ -415,7 +442,7 @@ public void asyncJson(final String url, final String body, Map h HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.setContentLength(body.length()); requestHeaders.set(RESTConstant.TASK_UUID, taskUuid); - requestHeaders.set(RESTConstant.CALLBACK_URL, callbackUrl); + requestHeaders.set(RESTConstant.CALLBACK_URL, selectCallbackUrl(url, headers, callbackUrl, port, path)); MediaType JSON = MediaType.parseMediaType("application/json; charset=utf-8"); requestHeaders.setContentType(JSON); if (headers != null) { diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java index 150c27ab41f..2dab5a39bf5 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java @@ -201,7 +201,7 @@ public void run(final FlowTrigger trigger, Map data) { callbackChecker.setUsername(getSelf().getSshUsername()); callbackChecker.setPassword(getSelf().getSshPassword()); callbackChecker.setPort(getSelf().getSshPort()); - callbackChecker.setCallbackIp(Platform.getManagementServerIp()); + callbackChecker.setCallbackIp(Platform.getManagementServerIpForRemote(getSelf().getHostname())); callbackChecker.setCallBackPort(CloudBusGlobalProperty.HTTP_PORT); AnsibleRunner runner = new AnsibleRunner(); diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java index e3ba7578f12..e9d5449b586 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java @@ -198,7 +198,7 @@ public void run(final FlowTrigger trigger, Map data) { callbackChecker.setUsername(getSelf().getSshUsername()); callbackChecker.setPassword(getSelf().getSshPassword()); callbackChecker.setPort(getSelf().getSshPort()); - callbackChecker.setCallbackIp(Platform.getManagementServerIp()); + callbackChecker.setCallbackIp(Platform.getManagementServerIpForRemote(getSelf().getHostname())); callbackChecker.setCallBackPort(CloudBusGlobalProperty.HTTP_PORT); AnsibleRunner runner = new AnsibleRunner(); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 4c6114c9ce0..89e0f8a4e45 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -6083,7 +6083,7 @@ public void run(final FlowTrigger trigger, Map data) { callbackChecker.setUsername(getSelf().getUsername()); callbackChecker.setPassword(getSelf().getPassword()); callbackChecker.setPort(getSelf().getPort()); - callbackChecker.setCallbackIp(Platform.getManagementServerIp()); + callbackChecker.setCallbackIp(Platform.getManagementServerIpForRemote(getSelf().getManagementIp())); callbackChecker.setCallBackPort(CloudBusGlobalProperty.HTTP_PORT); KvmHostConfigChecker kvmHostConfigChecker = new KvmHostConfigChecker(); @@ -6107,7 +6107,7 @@ public void run(final FlowTrigger trigger, Map data) { hostTcpConnectionCallbackChecker.setUsername(getSelf().getUsername()); hostTcpConnectionCallbackChecker.setPassword(getSelf().getPassword()); hostTcpConnectionCallbackChecker.setPort(getSelf().getPort()); - hostTcpConnectionCallbackChecker.setCallbackIp(Platform.getManagementServerIp()); + hostTcpConnectionCallbackChecker.setCallbackIp(Platform.getManagementServerIpForRemote(getSelf().getManagementIp())); hostTcpConnectionCallbackChecker.setCallBackPort(KVMGlobalProperty.TCP_SERVER_PORT); runner.installChecker(hostTcpConnectionCallbackChecker); } diff --git a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorage.java b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorage.java index cdc2622aa3f..debce4a6b13 100755 --- a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorage.java +++ b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorage.java @@ -392,7 +392,7 @@ private void connect(final Completion complete) { callbackChecker.setUsername(getSelf().getUsername()); callbackChecker.setPassword(getSelf().getPassword()); callbackChecker.setPort(getSelf().getSshPort()); - callbackChecker.setCallbackIp(Platform.getManagementServerIp()); + callbackChecker.setCallbackIp(Platform.getManagementServerIpForRemote(getSelf().getHostname())); callbackChecker.setCallBackPort(CloudBusGlobalProperty.HTTP_PORT); AnsibleRunner runner = new AnsibleRunner(); diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsPrimaryStorageMdsBase.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsPrimaryStorageMdsBase.java index bcd230bfdf7..416cc620572 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsPrimaryStorageMdsBase.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsPrimaryStorageMdsBase.java @@ -112,7 +112,7 @@ public void run(FlowTrigger trigger, Map data) { callBackChecker.setUsername(getSelf().getUsername()); callBackChecker.setPassword(getSelf().getPassword()); callBackChecker.setPort(getSelf().getPort()); - callBackChecker.setCallbackIp(Platform.getManagementServerIp()); + callBackChecker.setCallbackIp(Platform.getManagementServerIpForRemote(getSelf().getAddr())); callBackChecker.setCallBackPort(CloudBusGlobalProperty.HTTP_PORT); AnsibleRunner runner = new AnsibleRunner(); diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 3d8f64a1abe..7c84adfd6b6 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -249,6 +249,26 @@ class ManagementNetworkIpv6Case extends SubCase { "http://[2001:db8::1]:8080/zstack${RESTConstant.COMMAND_CHANNEL_PATH}" } + void testRestFacadeSelectsCallbackUrlByTargetIpVersion() { + withManagementServerIpProperties([ + "management.server.ip" : IPV4, + "management.server.ip6": IPV6, + ]) { + String defaultCallbackUrl = "http://${IPV4}:${REST_PORT}/zstack${RESTConstant.CALLBACK_PATH}" + + assert RESTFacadeImpl.selectCallbackUrl( + "http://[${IPV6_2}]:7070/host/ping", [:], defaultCallbackUrl, REST_PORT, "zstack") == + "http://[${IPV6}]:${REST_PORT}/zstack${RESTConstant.CALLBACK_PATH}" + assert RESTFacadeImpl.selectCallbackUrl( + "http://host.example.com:7070/host/ping", [:], defaultCallbackUrl, REST_PORT, "zstack") == + defaultCallbackUrl + assert RESTFacadeImpl.selectCallbackUrl( + "http://[${IPV6_2}]:7070/host/ping", + [(RESTConstant.CALLBACK_URL): "http://override.example.com/callback"], + defaultCallbackUrl, REST_PORT, "zstack") == defaultCallbackUrl + } + } + void testSshTargetUsesRawIpv6Host() { assert SshShell.formatSshTarget("root", IPV4) == "root@192.168.1.10" assert SshShell.formatSshTarget("root", IPV6) == "root@2001:db8::1" @@ -357,6 +377,25 @@ class ManagementNetworkIpv6Case extends SubCase { } } + void testSelectManagementServerIpForRemote() { + withManagementServerIpProperties([ + "management.server.ip" : IPV4, + "management.server.ip6": IPV6, + ]) { + assert Platform.selectManagementServerIpForRemote(IPV6_2, null) == IPV6 + assert Platform.selectManagementServerIpForRemote(IPV6_2, "2001:db8::88") == "2001:db8::88" + assert Platform.selectManagementServerIpForRemote("host.example.com", null) == IPV4 + } + + withManagementServerIpProperties([ + "management.server.ip" : IPV6, + "management.server.ip4": IPV4, + ]) { + assert Platform.selectManagementServerIpForRemote("192.168.1.20", null) == IPV4 + assert Platform.selectManagementServerIpForRemote("192.168.1.20", "192.168.1.88") == "192.168.1.88" + } + } + void testManagementServerSecondaryPropertyRejectsWrongAddressFamily() { withManagementServerIpProperties([ "management.server.ip" : IPV4, From c58b24eac04b4649daf7c967f7cce6da467e746f Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 2 Jul 2026 19:08:01 +0900 Subject: [PATCH 2/2] [core]: avoid route shell in callback Resolve review comment on MR !10385. Generic REST callback selection now uses configured MN addresses by target IP version and does not invoke ip route get on async REST hot path. Non-configured route source candidates are ignored. Resolves: ZSTAC-86567 Change-Id: Ia9a34bbe87c21067643e3bbfb410e8b501d25eb3 --- .../main/java/org/zstack/core/Platform.java | 22 ++++++++++++------- .../core/ManagementNetworkIpv6Case.groovy | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index fa38efcb033..34df20e3e12 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1388,12 +1388,7 @@ public static String getRouteSourceIp(String remoteIp) { } public static String getManagementServerIpForRemote(String remoteIp) { - if (StringUtils.isBlank(remoteIp)) { - return getManagementServerIp(); - } - - remoteIp = normalizeManagementIp(remoteIp); - return selectManagementServerIpForRemote(remoteIp, getRouteSourceIp(remoteIp)); + return selectManagementServerIpForRemote(remoteIp, null); } public static String selectManagementServerIpForRemote(String remoteIp, String routeSourceIp) { @@ -1404,7 +1399,7 @@ public static String selectManagementServerIpForRemote(String remoteIp, String r remoteIp = normalizeManagementIp(remoteIp); routeSourceIp = normalizeManagementIp(routeSourceIp); if (IPv6NetworkUtils.isIpv6Address(remoteIp)) { - if (IPv6NetworkUtils.isIpv6Address(routeSourceIp)) { + if (IPv6NetworkUtils.isIpv6Address(routeSourceIp) && isManagementServerIp(routeSourceIp)) { return routeSourceIp; } String ip6 = getManagementServerIp6(); @@ -1412,7 +1407,7 @@ public static String selectManagementServerIpForRemote(String remoteIp, String r } if (NetworkUtils.isIpv4Address(remoteIp)) { - if (NetworkUtils.isIpv4Address(routeSourceIp)) { + if (NetworkUtils.isIpv4Address(routeSourceIp) && isManagementServerIp(routeSourceIp)) { return routeSourceIp; } String ip4 = getManagementServerIp4(); @@ -1422,6 +1417,17 @@ public static String selectManagementServerIpForRemote(String remoteIp, String r return getManagementServerIp(); } + private static boolean isManagementServerIp(String ip) { + if (StringUtils.isBlank(ip)) { + return false; + } + + String normalizedIp = normalizeManagementIp(ip); + return getManagementServerIps().stream() + .map(Platform::normalizeManagementIp) + .anyMatch(normalizedIp::equals); + } + public static String selectManagementServerIp(Collection addresses) { String ipv4 = null; String ipv6 = null; diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 7c84adfd6b6..c8a407abd20 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -383,7 +383,7 @@ class ManagementNetworkIpv6Case extends SubCase { "management.server.ip6": IPV6, ]) { assert Platform.selectManagementServerIpForRemote(IPV6_2, null) == IPV6 - assert Platform.selectManagementServerIpForRemote(IPV6_2, "2001:db8::88") == "2001:db8::88" + assert Platform.selectManagementServerIpForRemote(IPV6_2, "2001:db8::88") == IPV6 assert Platform.selectManagementServerIpForRemote("host.example.com", null) == IPV4 } @@ -392,7 +392,7 @@ class ManagementNetworkIpv6Case extends SubCase { "management.server.ip4": IPV4, ]) { assert Platform.selectManagementServerIpForRemote("192.168.1.20", null) == IPV4 - assert Platform.selectManagementServerIpForRemote("192.168.1.20", "192.168.1.88") == "192.168.1.88" + assert Platform.selectManagementServerIpForRemote("192.168.1.20", "192.168.1.88") == IPV4 } }