Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.zstack.header.message.APIMessage;
import org.zstack.utils.ShellResult;
import org.zstack.utils.ShellUtils;
import org.zstack.utils.network.IPv6Utils;
import org.zstack.utils.network.NetworkUtils;

import static org.zstack.core.Platform.argerr;
Expand Down Expand Up @@ -121,9 +122,13 @@ private void validate(APIUpdateHostMsg msg) {
}

private void validate(APIAddHostMsg msg) {
if (!NetworkUtils.isIpv4Address(msg.getManagementIp()) && !NetworkUtils.isHostname(msg.getManagementIp())) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_HOST_10113, "managementIp[%s] is neither an IPv4 address nor a valid hostname", msg.getManagementIp()));
String ip = msg.getManagementIp();
if (!NetworkUtils.isHostname(ip) && !IPv6Utils.isValidManagementIp(ip)) {
throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_HOST_10113,
"managementIp[%s] is not a valid IPv4/IPv6 address or hostname", ip));
}
// Normalize IPv6 to RFC 5952 compressed form; IPv4/hostname returned as-is
msg.setManagementIp(IPv6Utils.normalizeIpv6(ip));
}

private void validate(APIChangeHostStateMsg msg){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.zstack.header.message.Message;
import org.zstack.header.vm.*;
import org.zstack.utils.Utils;
import org.zstack.utils.network.IPv6Utils;
import org.zstack.utils.logging.CLogger;

import javax.persistence.Query;
Expand Down Expand Up @@ -125,12 +126,14 @@ public void fail(ErrorCode errorCode) {
}

private void overriddenConsoleProxyIP(ConsoleInventory consoleInventory) {
String ip;
if (!"0.0.0.0".equals(CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP) &&
!"".equals(CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP)) {
consoleInventory.setHostname(CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP);
ip = CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP;
} else {
consoleInventory.setHostname(CoreGlobalProperty.UNIT_TEST_ON ? "127.0.0.1" : Platform.getManagementServerIp());
ip = CoreGlobalProperty.UNIT_TEST_ON ? "127.0.0.1" : Platform.getManagementServerIp();
}
consoleInventory.setHostname(IPv6Utils.bracketIpv6(ip));
}

@Override
Expand Down
150 changes: 116 additions & 34 deletions core/src/main/java/org/zstack/core/Platform.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import com.googlecode.ipv6.IPv6Network;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import org.zstack.core.config.NetworkGlobalConfig;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.TimeUnit;
Expand All @@ -74,8 +77,8 @@ public class Platform {

private static ComponentLoader loader;
private static String msId;
private static String managementServerIp;
private static String managementServerCidr;
private static volatile String managementServerIp;
private static volatile String managementServerCidr;
private static MessageSource messageSource;
private static String encryptionKey = EncryptRSA.generateKeyString("ZStack open source");
private static EncryptRSA rsa = new EncryptRSA();
Expand Down Expand Up @@ -395,6 +398,17 @@ private static void prepareDefaultDbProperties() {
}
}

/**
* F-010: JGroups initial_hosts IPv6 bracket fix.
* IPv6 addresses in JGroups must use [addr][port] format; IPv4 uses addr[port] format.
*/
private static String jgroupsAddr(String ip, String port) {
if (IPv6NetworkUtils.isIpv6Address(ip)) {
return "[" + ip + "][" + port + "]";
}
return ip + "[" + port + "]";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private static void prepareHibernateSearchProperties() {
if (!SearchGlobalProperty.SearchAutoRegister) {
System.setProperty("Search.autoRegister", "false");
Expand Down Expand Up @@ -442,12 +456,12 @@ private static void prepareHibernateSearchProperties() {
if (info.getPeerip() == null) {
throw new RuntimeException("the ip of peer node was null, please check the config of zsha2");
}
SearchGlobalProperty.JGroupInfinispanInitialHosts = String.format("%s[%s],%s[%s]",
info.getNodeip(), SearchGlobalProperty.JGroupInfinispanPort,
info.getPeerip(), SearchGlobalProperty.JGroupInfinispanPort);
SearchGlobalProperty.JGroupBackendInitialHosts = String.format("%s[%s],%s[%s]",
info.getNodeip(), SearchGlobalProperty.JGroupBackendPort,
info.getPeerip(), SearchGlobalProperty.JGroupBackendPort);
SearchGlobalProperty.JGroupInfinispanInitialHosts =
jgroupsAddr(info.getNodeip(), SearchGlobalProperty.JGroupInfinispanPort) + "," +
jgroupsAddr(info.getPeerip(), SearchGlobalProperty.JGroupInfinispanPort);
SearchGlobalProperty.JGroupBackendInitialHosts =
jgroupsAddr(info.getNodeip(), SearchGlobalProperty.JGroupBackendPort) + "," +
jgroupsAddr(info.getPeerip(), SearchGlobalProperty.JGroupBackendPort);
if (getGlobalProperty("JGroup.TcppingInitialHosts") == null) {
System.setProperty("JGroup.InfinispanInitialHosts", SearchGlobalProperty.JGroupInfinispanInitialHosts);
logger.debug(String.format("default JGroup.InfinispanInitialHosts to JGroup.InfinispanInitialHosts [%s]", SearchGlobalProperty.JGroupInfinispanInitialHosts));
Expand Down Expand Up @@ -788,6 +802,11 @@ public static boolean isVIPNode() {
private static String getManagementServerCidrInternal() {
String mgtIp = getManagementServerIp();

// F-003: IPv6 management IP uses a dedicated code path
if (IPv6NetworkUtils.isIpv6Address(mgtIp)) {
return getManagementServerCidrIpv6(mgtIp);
}

/*# ip add | grep 10.86.4.132
inet 10.86.4.132/23 brd 10.86.5.255 scope global br_eth0*/
/* because Linux.shell can not run command with '|', pares the output of ip address in java */
Expand All @@ -806,6 +825,53 @@ private static String getManagementServerCidrInternal() {
return null;
}

/**
* F-003: 当管理 IP 为 IPv6 时,通过 'ip -6 addr show' 解析对应的网络 CIDR。
*/
private static String getManagementServerCidrIpv6(String mnIp) {
try {
Linux.ShellResult result = Linux.shell("ip -6 addr show");
if (result.getExitCode() != 0) {
logger.warn(String.format("failed to run 'ip -6 addr show', exit code: %d", result.getExitCode()));
return null;
}
// 示例行: " inet6 2001:db8::1/64 scope global"
for (String line : result.getStdout().split("\n")) {
String trimmed = line.trim();
if (!trimmed.startsWith("inet6 ")) {
continue;
}
String[] parts = trimmed.split("\\s+");
if (parts.length < 2) {
continue;
}
String cidr = parts[1]; // "2001:db8::1/64"
String[] ipPrefix = cidr.split("/");
if (ipPrefix.length != 2) {
continue;
}
try {
String addr = InetAddress.getByName(ipPrefix[0]).getHostAddress();
String mnNorm = InetAddress.getByName(mnIp).getHostAddress();
if (addr.equals(mnNorm)) {
int prefixLen = Integer.parseInt(ipPrefix[1]);
// compute network address prefix, e.g. 2001:db8::/64
String networkAddr = IPv6Network.fromString(ipPrefix[0] + "/" + prefixLen)
.getFirst().toString();
return networkAddr + "/" + prefixLen;
}
} catch (UnknownHostException | NumberFormatException ignore) {
// skip lines that cannot be parsed
}
}
logger.warn(String.format("no inet6 entry found for MN IP: %s", mnIp));
return null;
} catch (IOException e) {
logger.warn(String.format("failed to get IPv6 CIDR for MN IP %s: %s", mnIp, e.getMessage()));
return null;
}
}

public static String getManagementServerCidr() {
if (managementServerCidr == null) {
managementServerCidr = getManagementServerCidrInternal();
Expand All @@ -827,45 +893,61 @@ private static String getManagementServerIpInternal() {
return ip;
}

Linux.ShellResult ret = Linux.shell("/sbin/ip route");
String defaultLine = null;
for (String s : ret.getStdout().split("\n")) {
if (s.contains("default via")) {
defaultLine = s;
break;
}
// F-002: IPv6 support — enumerate all NICs, prioritise based on PREFER_IPV6 config
boolean preferIpv6 = false;
try {
preferIpv6 = NetworkGlobalConfig.PREFER_IPV6.value(Boolean.class);
} catch (Exception ignored) {
// GlobalConfig 可能在静态初始化阶段还未就绪,安全降级为 false
}

String err = "cannot get management server ip of this machine. there are three ways to get the ip.\n1) search for 'management.server.ip' java property\n2) search for 'ZSTACK_MANAGEMENT_SERVER_IP' environment variable\n3) search for default route printed out by '/sbin/ip route'\nhowever, all above methods failed";
if (defaultLine == null) {
throw new CloudRuntimeException(err);
}
List<InetAddress> ipv4List = new ArrayList<>();
List<InetAddress> ipv6List = new ArrayList<>();

try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface iface : Collections.list(nets)) {
String name = iface.getName();
if (defaultLine.contains(name)) {
for (InetAddress ia : Collections.list(iface.getInetAddresses())) {
ip = ia.getHostAddress();
if (ia instanceof Inet4Address) {
// we prefer IPv4 address
ip = ia.getHostAddress();
break;
}
Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();
if (ifaces == null) {
throw new IllegalStateException("no available network interfaces");
}
while (ifaces.hasMoreElements()) {
NetworkInterface iface = ifaces.nextElement();
if (iface.isLoopback() || !iface.isUp()) {
continue;
}
Enumeration<InetAddress> addrs = iface.getInetAddresses();
while (addrs.hasMoreElements()) {
InetAddress addr = addrs.nextElement();
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
continue;
}
if (addr instanceof Inet4Address) {
ipv4List.add(addr);
} else if (addr instanceof Inet6Address) {
ipv6List.add(addr);
}
}
}
} catch (SocketException e) {
throw new CloudRuntimeException(e);
}

if (ip == null) {
throw new CloudRuntimeException(err);
List<InetAddress> preferred = preferIpv6 ? ipv6List : ipv4List;
List<InetAddress> fallback = preferIpv6 ? ipv4List : ipv6List;

if (!preferred.isEmpty()) {
ip = preferred.get(0).getHostAddress();
logger.info(String.format("get management IP[%s] from network interface enumeration (prefer IPv%s)",
ip, preferIpv6 ? "6" : "4"));
return ip;
}
if (!fallback.isEmpty()) {
ip = fallback.get(0).getHostAddress();
logger.info(String.format("get management IP[%s] from network interface enumeration (fallback IPv%s)",
ip, preferIpv6 ? "4" : "6"));
return ip;
}

logger.info(String.format("get management IP[%s] from default route[/sbin/ip route]", ip));
return ip;
throw new IllegalStateException("no available management IP found on any network interface");
}

public static String toI18nString(String code, Object... args) {
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/java/org/zstack/core/config/NetworkGlobalConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.zstack.core.config;

/**
* Global configuration entries for network-related settings.
*/
@GlobalConfigDefinition
public class NetworkGlobalConfig {
public static final String CATEGORY = "network";

@GlobalConfigValidation(validValues = {"true", "false"})
@GlobalConfigDef(
type = Boolean.class,
defaultValue = "false",
description = "When true, the management node prefers IPv6 addresses on dual-stack hosts. " +
"Has no effect on IPv4-only or IPv6-only hosts."
)
public static GlobalConfig PREFER_IPV6 = new GlobalConfig(CATEGORY, "management.server.prefer.ipv6");
}
31 changes: 28 additions & 3 deletions core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
import org.zstack.utils.Utils;
import org.zstack.utils.gson.JSONObjectUtil;
import org.zstack.utils.logging.CLogger;
import org.zstack.utils.network.IPv6NetworkUtils;
import org.zstack.utils.network.IPv6Utils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
Expand All @@ -63,6 +65,8 @@

public class RESTFacadeImpl implements RESTFacade {
private static final CLogger logger = Utils.getSafeLogger(RESTFacadeImpl.class);
private static final java.util.regex.Pattern BARE_IPV6_URL_PATTERN = java.util.regex.Pattern.compile(
"^(https?)://([0-9a-fA-F][0-9a-fA-F:]{2,37}[0-9a-fA-F]):(\\d+)(/.*)?");

@Autowired
private ThreadFacade thdf;
Expand Down Expand Up @@ -194,13 +198,13 @@ void init() {

String url;
if ("".equals(path) || path == null) {
url = String.format("http://%s:%s", callbackHostName, port);
url = IPv6Utils.buildUrl(callbackHostName, port);
} else {
url = String.format("http://%s:%s/%s", callbackHostName, port, path);
url = IPv6Utils.buildUrl(callbackHostName, port) + "/" + path;
}
UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(url);
ub.path(RESTConstant.CALLBACK_PATH);
callbackUrl = ub.build().toUriString();
callbackUrl = sanitizeCallbackUrl(ub.build().toUriString());

ub = UriComponentsBuilder.fromHttpUrl(url);
baseUrl = ub.build().toUriString();
Expand Down Expand Up @@ -977,6 +981,27 @@ public String getCallbackUrl() {
return callbackUrl;
}

/**
* 检测并修复裸 IPv6(无方括号)的 callbackUrl。
* 正常路径下 URL 应由 {@link IPv6Utils#buildUrl} 生成,此方法作为兜底防御层。
* <p>
* 示例:http://2001:db8::1:8080/path → http://[2001:db8::1]:8080/path
*/
private static String sanitizeCallbackUrl(String url) {
if (url == null) {
return null;
}
// 检测裸 IPv6 URL 模式:scheme://hex:chars:port/path(IP 未被方括号包裹)
java.util.regex.Matcher m = BARE_IPV6_URL_PATTERN.matcher(url);
if (m.matches() && IPv6NetworkUtils.isIpv6Address(m.group(2))) {
String corrected = m.group(1) + "://[" + m.group(2) + "]:" + m.group(3) +
(m.group(4) != null ? m.group(4) : "");
logger.warn(String.format("bare IPv6 in callbackUrl, auto-corrected: %s -> %s", url, corrected));
return corrected;
}
return url;
}

@Override
public String getHostName() {
return callbackHostName;
Expand Down
Loading