From 766dc991427f2b6ddb10c9f03acf1475a5953a3a Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Fri, 4 Sep 2026 22:37:21 +0200 Subject: [PATCH] utils: do not require an IPv4 default route to find the default NIC A management server does not need an IPv4 default route. It can be IPv6-first, or reach its POD network and KVM agents over IPv4 without having a default gateway on that family at all. getDefaultEthDevice() only looked at the IPv4 routing table and picked column 5 of the route as the device name: ip route show default 0.0.0.0/0 | head -1 | awk '{print $5}' That has two problems: * Anything the shell or 'ip' writes on stderr ends up in the captured output (Script merges stderr into stdout), so the method can return a string that is not a device name at all. getAllDefaultNicIps() then passes it to NetworkInterface.getByName(), which returns null, and the unchecked dereference throws a NullPointerException. On a host without an IPv4 default route this aborts the boot of the management server: ERROR [o.a.c.s.l.CloudStackExtendedLifeCycle] Error on configuring bean RootCAProvider - Cannot invoke "java.net.NetworkInterface.getInterfaceAddresses()" because "nic" is null at com.cloud.utils.net.NetUtils.getAllDefaultNicIps(NetUtils.java:298) at o.a.c.ca.provider.RootCAProvider.loadManagementKeyStore(RootCAProvider.java:409) ERROR [o.a.c.s.m.m.i.DefaultModuleDefinitionSet] Failed to load module [root-ca] * Column 5 is only the device for routes of the form "default via dev ...". For an on-link default route such as "default dev eth0 scope link" or "default dev eno1 proto kernel metric 256" it returns "link" or "kernel". The device name is now taken from the token following "dev", which is correct for every route layout including multipath routes, and the IPv6 routing table is consulted when there is no IPv4 default route. The results of NetworkInterface.getByName() are null checked in both getDefaultHostIp() and getAllDefaultNicIps() so an unresolvable device name degrades to "no default NIC" instead of an exception. --- .../java/com/cloud/utils/net/NetUtils.java | 30 ++++++++++++++- .../com/cloud/utils/net/NetUtilsTest.java | 38 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/utils/src/main/java/com/cloud/utils/net/NetUtils.java b/utils/src/main/java/com/cloud/utils/net/NetUtils.java index d89d9fa2d93c..03cc4f029e36 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -267,6 +267,11 @@ public static String getDefaultHostIp() { return null; } + if (nic == null) { + LOGGER.warn("Unable to find a network interface named [{}], cannot determine the default host IP.", pubNic); + return null; + } + String[] info = null; try { info = NetUtils.getNetworkParams(nic); @@ -295,6 +300,11 @@ public static List getAllDefaultNicIps() { return addrs; } + if (nic == null) { + LOGGER.warn("Unable to find a network interface named [{}], no default NIC IPs will be returned.", pubNic); + return addrs; + } + for (InterfaceAddress address : nic.getInterfaceAddresses()) { addrs.add(address.getAddress().getHostAddress().split("%")[0]); } @@ -306,7 +316,25 @@ public static String getDefaultEthDevice() { final String defDev = Script.runSimpleBashScript("/sbin/route -n get default 2> /dev/null | grep interface | awk '{print $2}'"); return defDev; } - return Script.runSimpleBashScript("ip route show default 0.0.0.0/0 | head -1 | awk '{print $5}'"); + final String defaultIp4Device = getDefaultEthDevice(false); + if (defaultIp4Device != null) { + return defaultIp4Device; + } + LOGGER.debug("No IPv4 default route found, falling back to the IPv6 default route to determine the default network device."); + return getDefaultEthDevice(true); + } + + /** + * Returns the name of the network device used by the IPv4 or IPv6 default route, or null when there is no such + * default route. + * + * An IPv4 default route is not a requirement, a host can be IPv6-only or have IPv4 connectivity without a default + * route, therefore both address families are looked up separately. + */ + protected static String getDefaultEthDevice(final boolean ipv6) { + final String command = String.format("ip -%d route show default | awk '{for (i = 1; i < NF; i++) if ($i == \"dev\") {print $(i + 1); exit}}'", + ipv6 ? 6 : 4); + return Script.runSimpleBashScript(command); } public static String getLocalIPString() { diff --git a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java index 5c9d41f90a25..9a3ab18ba592 100644 --- a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java @@ -43,12 +43,15 @@ import java.util.TreeSet; import java.util.stream.Collectors; +import org.apache.commons.lang3.SystemUtils; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils.SupersetOrSubset; +import com.cloud.utils.script.Script; import com.googlecode.ipv6.IPv6Address; import com.googlecode.ipv6.IPv6Network; @@ -748,6 +751,41 @@ public void testAllIpsOfDefaultNic() { } } + @Test + public void testAllIpsOfDefaultNicWhenDeviceDoesNotExist() { + try (MockedStatic