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/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)) 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']