From c1fa82812e5ce5d0dd3a5af4c7b0a2981b6f4c54 Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Wed, 5 Aug 2026 06:49:55 +0200 Subject: [PATCH 1/2] systemvm: enable IPv6 link-local on the control network Bring up an EUI-64 based IPv6 link-local address on the control NIC of system VMs and make sshd listen on it, in addition to the existing 169.254.0.0/16 IPv4 address. Since the address is generated with EUI-64 it can be calculated from the MAC address of the control NIC. listSystemVms now returns it as 'linklocalip6', calculated by the Management Server using NetUtils.ipv6LinkLocal(), and the UI shows it in the System VMs view. On KVM the agent enables IPv6 (link-local only, no RA/SLAAC) on the control bridge (cloud0) when setting up the control network, so the host can reach system VMs over IPv6 once this is used. --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../api/response/SystemVmResponse.java | 12 +++++++ .../kvm/resource/BridgeVifDriver.java | 1 + .../hypervisor/kvm/resource/IvsVifDriver.java | 1 + .../hypervisor/kvm/resource/OvsVifDriver.java | 1 + .../kvm/resource/VifDriverBase.java | 13 +++++++ .../java/com/cloud/api/ApiResponseHelper.java | 3 ++ systemvm/debian/opt/cloud/bin/setup/common.sh | 34 ++++++++++++++++++- ui/public/locales/en.json | 1 + ui/src/config/section/infra/systemVms.js | 4 +-- 10 files changed, 68 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index ac6acdf42516..3232f04733cb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -743,6 +743,7 @@ public class ApiConstants { public static final String LINMIN_APID = "linminapid"; public static final String DHCP_SERVER_TYPE = "dhcpservertype"; public static final String LINK_LOCAL_IP = "linklocalip"; + public static final String LINK_LOCAL_IP6 = "linklocalip6"; public static final String LINK_LOCAL_MAC_ADDRESS = "linklocalmacaddress"; public static final String LINK_LOCAL_MAC_NETMASK = "linklocalnetmask"; public static final String LINK_LOCAL_NETWORK_ID = "linklocalnetworkid"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java index a3ed88c27356..1cf84c8e3b88 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java @@ -106,6 +106,10 @@ public class SystemVmResponse extends BaseResponseWithAnnotations { @Param(description = "The Control IP address for the System VM") private String linkLocalIp; + @SerializedName(ApiConstants.LINK_LOCAL_IP6) + @Param(description = "The Control IPv6 link-local address for the System VM, calculated from the link local MAC address", since = "4.23.0") + private String linkLocalIp6; + @SerializedName(ApiConstants.LINK_LOCAL_MAC_ADDRESS) @Param(description = "The link local MAC address for the System VM") private String linkLocalMacAddress; @@ -427,6 +431,14 @@ public void setLinkLocalIp(String linkLocalIp) { this.linkLocalIp = linkLocalIp; } + public String getLinkLocalIp6() { + return linkLocalIp6; + } + + public void setLinkLocalIp6(String linkLocalIp6) { + this.linkLocalIp6 = linkLocalIp6; + } + public String getLinkLocalMacAddress() { return linkLocalMacAddress; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java index 327ec46e0ecf..d57bede65a63 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java @@ -510,6 +510,7 @@ public void createControlNetwork(String privBrName) { Script.runSimpleBashScript("ip link set " + privBrName + " up"); Script.runSimpleBashScript("ip address add " + NetUtils.getLinkLocalAddressFromCIDR(_controlCidr) + " dev " + privBrName); } + enableBridgeIpv6LinkLocal(privBrName); } @Override diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/IvsVifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/IvsVifDriver.java index bbe87aa13735..92f03032967a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/IvsVifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/IvsVifDriver.java @@ -279,6 +279,7 @@ public void createControlNetwork(String privBrName) { Script.runSimpleBashScript("ip link add " + privBrName + " type bridge; ip link set " + privBrName + " up"); Script.runSimpleBashScript("ip address add " + NetUtils.getLinkLocalAddressFromCIDR(_controlCidr) + " dev " + privBrName, _timeout); } + enableBridgeIpv6LinkLocal(privBrName); } @Override diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/OvsVifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/OvsVifDriver.java index 4c0482c5384f..d938f1788baa 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/OvsVifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/OvsVifDriver.java @@ -250,6 +250,7 @@ public void createControlNetwork(String privBrName) { if (!isExistingBridge(privBrName)) { Script.runSimpleBashScript("ovs-vsctl add-br " + privBrName + "; ip link set " + privBrName + " up; ip address add " + NetUtils.getLinkLocalAddressFromCIDR(_controlCidr) + " dev " + privBrName, _timeout); } + enableBridgeIpv6LinkLocal(privBrName); } @Override diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriverBase.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriverBase.java index 2ad2b8a6d4ce..c4e9752a9a5e 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriverBase.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/VifDriverBase.java @@ -31,6 +31,7 @@ import com.cloud.agent.api.to.NicTO; import com.cloud.exception.InternalErrorException; +import com.cloud.utils.script.Script; public abstract class VifDriverBase implements VifDriver { @@ -81,6 +82,18 @@ public boolean isExistingBridge(String bridgeName) { return false; } + /** + * Enable IPv6 on the control network bridge so the host can reach the + * IPv6 link-local address system VMs listen on. Only link-local is wanted, + * so Router Advertisements and SLAAC are disabled on the bridge. + */ + protected void enableBridgeIpv6LinkLocal(String bridgeName) { + logger.info("Enabling IPv6 link-local on bridge {}", bridgeName); + Script.runSimpleBashScript("sysctl -qw net.ipv6.conf." + bridgeName + ".accept_ra=0" + + " net.ipv6.conf." + bridgeName + ".autoconf=0" + + " net.ipv6.conf." + bridgeName + ".disable_ipv6=0"); + } + protected static int getNetworkRateKbps(NicTO nic) { if (nic.getNetworkRateMbps() != null && nic.getNetworkRateMbps().intValue() != -1) { return nic.getNetworkRateMbps().intValue() * bitsPerMbpsToKbps; diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 2510dc0a88b4..d715a20e0610 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -1886,6 +1886,9 @@ public SystemVmResponse createSystemVmResponse(VirtualMachine vm) { vmResponse.setLinkLocalIp(singleNicProfile.getIPv4Address()); vmResponse.setLinkLocalMacAddress(singleNicProfile.getMacAddress()); vmResponse.setLinkLocalNetmask(singleNicProfile.getIPv4Netmask()); + if (singleNicProfile.getMacAddress() != null) { + vmResponse.setLinkLocalIp6(NetUtils.ipv6LinkLocal(singleNicProfile.getMacAddress()).toString()); + } } else if (network.getTrafficType() == TrafficType.Public) { vmResponse.setPublicIp(singleNicProfile.getIPv4Address()); vmResponse.setPublicMacAddress(singleNicProfile.getMacAddress()); diff --git a/systemvm/debian/opt/cloud/bin/setup/common.sh b/systemvm/debian/opt/cloud/bin/setup/common.sh index ef1576ab588c..825b03b6a2c4 100755 --- a/systemvm/debian/opt/cloud/bin/setup/common.sh +++ b/systemvm/debian/opt/cloud/bin/setup/common.sh @@ -573,10 +573,42 @@ setup_dnsmasq() { fi } +enable_ipv6_link_local() { + local eth=$1 + log_it "Enabling IPv6 link-local on interface $eth" + # Generate the link-local address with EUI-64 based on the MAC address so + # the address can be calculated by the Management Server + sysctl -w net.ipv6.conf.${eth}.addr_gen_mode=0 + # Only a link-local address is wanted, no SLAAC/RA configuration + sysctl -w net.ipv6.conf.${eth}.accept_ra=0 + sysctl -w net.ipv6.conf.${eth}.autoconf=0 + sysctl -w net.ipv6.conf.${eth}.disable_ipv6=0 + + # Wait for Duplicate Address Detection to complete so the address can be bound + LINK_LOCAL_IP6="" + local i + for i in $(seq 1 10); do + LINK_LOCAL_IP6=$(ip -6 addr show dev ${eth} scope link -tentative | grep -Po '(?<=inet6 )fe80:[0-9a-f:]+' | head -1) + [ -n "$LINK_LOCAL_IP6" ] && break + sleep 1 + done + + if [ -n "$LINK_LOCAL_IP6" ]; then + log_it "Interface $eth has IPv6 link-local address $LINK_LOCAL_IP6" + else + log_it "No IPv6 link-local address appeared on interface $eth" + fi +} + setup_sshd(){ local ip=$1 local eth=$2 - [ -f /etc/ssh/sshd_config ] && sed -i -e "s/^[#]*ListenAddress.*$/ListenAddress $ip/" /etc/ssh/sshd_config + [ -f /etc/ssh/sshd_config ] && sed -i -e "/^ListenAddress fe80/d" -e "s/^[#]*ListenAddress.*$/ListenAddress $ip/" /etc/ssh/sshd_config + enable_ipv6_link_local $eth + if [ -n "$LINK_LOCAL_IP6" ]; then + log_it "Configuring sshd to also listen on ${LINK_LOCAL_IP6}%${eth}" + sed -i -e "/^ListenAddress $ip$/a ListenAddress ${LINK_LOCAL_IP6}%${eth}" /etc/ssh/sshd_config + fi sed -i "/3922/s/eth./$eth/" /etc/iptables/rules.v4 } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 775de26103a0..d8c678ad68b3 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1599,6 +1599,7 @@ "label.link": "Link", "label.link.domain.to.ldap": "Link domain to LDAP", "label.linklocalip": "Link-local/Control IP address", +"label.linklocalip6": "Link-local/Control IPv6 address", "label.linux": "Linux", "label.list.ciscoasa1000v": "ASA 1000v", "label.list.ciscovnmc": "Cisco VNMC", diff --git a/ui/src/config/section/infra/systemVms.js b/ui/src/config/section/infra/systemVms.js index 4a5879b17626..c40826e42884 100644 --- a/ui/src/config/section/infra/systemVms.js +++ b/ui/src/config/section/infra/systemVms.js @@ -25,8 +25,8 @@ export default { docHelp: 'adminguide/systemvm.html', permission: ['listSystemVms'], searchFilters: ['name', 'zoneid', 'podid', 'hostid', 'systemvmtype', 'storageid', 'arch'], - columns: ['name', 'state', 'agentstate', 'systemvmtype', 'publicip', 'privateip', 'linklocalip', 'version', 'hostname', 'arch', 'zonename'], - details: ['name', 'id', 'agentstate', 'systemvmtype', 'publicip', 'privateip', 'linklocalip', 'gateway', 'hostname', 'arch', 'version', 'zonename', 'created', 'activeviewersessions', 'isdynamicallyscalable', 'hostcontrolstate', 'storageip'], + columns: ['name', 'state', 'agentstate', 'systemvmtype', 'publicip', 'privateip', 'linklocalip', 'linklocalip6', 'version', 'hostname', 'arch', 'zonename'], + details: ['name', 'id', 'agentstate', 'systemvmtype', 'publicip', 'privateip', 'linklocalip', 'linklocalip6', 'gateway', 'hostname', 'arch', 'version', 'zonename', 'created', 'activeviewersessions', 'isdynamicallyscalable', 'hostcontrolstate', 'storageip'], resourceType: 'SystemVm', filters: () => { const filters = ['starting', 'running', 'stopping', 'stopped', 'destroyed', 'expunging', 'migrating', 'error', 'unknown', 'shutdown'] From 137b3da610c1902b23afdb8271e64ddb825c9f6b Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Wed, 5 Aug 2026 07:04:36 +0200 Subject: [PATCH 2/2] systemvm: allow link-local ssh in the VR IPv6 firewall The nftables ip6_firewall and ip6_acl tables created on VRs with IPv6 networking have an input hook chain with policy drop, which applies to all interfaces including the control NIC. Accept TCP 3922 between link-local addresses so sshd remains reachable on the IPv6 link-local address of the control interface. Restricting both saddr and daddr to fe80::/10 ensures no global address can reach sshd. --- systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py b/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py index 63d7724dd20a..d8b8b5df8d6a 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsNetfilter.py @@ -232,6 +232,10 @@ def add_ip6_chain(self, address_family, table, chain, hook, action): if hook == "input" or hook == "output": CsHelper.execute("nft add rule %s %s %s icmpv6 type { echo-request, echo-reply, \ nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept" % (address_family, table, chain)) + if hook == "input": + # sshd listens on the IPv6 link-local address of the control interface, + # only allow this over link-local so no global address can reach it + CsHelper.execute("nft add rule %s %s %s ip6 saddr fe80::/10 ip6 daddr fe80::/10 tcp dport 3922 accept" % (address_family, table, chain)) if hook == "input" or hook == "forward": CsHelper.execute("nft add rule %s %s %s ct state established,related accept" % (address_family, table, chain))