diff --git a/api/src/main/java/com/cloud/agent/api/to/StaticNatRuleTO.java b/api/src/main/java/com/cloud/agent/api/to/StaticNatRuleTO.java index 8064c301991a..9c817f2debb7 100644 --- a/api/src/main/java/com/cloud/agent/api/to/StaticNatRuleTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/StaticNatRuleTO.java @@ -28,6 +28,7 @@ public class StaticNatRuleTO extends FirewallRuleTO { String dstIp; + boolean shouldApplyCrossNetworkSnat = false; protected StaticNatRuleTO() { } @@ -79,4 +80,12 @@ public String getDstIp() { return dstIp; } + public boolean isShouldApplyCrossNetworkSnat() { + return shouldApplyCrossNetworkSnat; + } + + public void setShouldApplyCrossNetworkSnat(boolean shouldApplyCrossNetworkSnat) { + this.shouldApplyCrossNetworkSnat = shouldApplyCrossNetworkSnat; + } + } diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetStaticNatRulesConfigItem.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetStaticNatRulesConfigItem.java index 0439b168a9da..af0332b70f6b 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetStaticNatRulesConfigItem.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetStaticNatRulesConfigItem.java @@ -39,7 +39,8 @@ public List generateConfig(final NetworkElementCommand cmd) { final LinkedList rules = new LinkedList<>(); for (final StaticNatRuleTO rule : command.getRules()) { - final StaticNatRule staticNatRule = new StaticNatRule(rule.revoked(), rule.getProtocol(), rule.getSrcIp(), rule.getStringSrcPortRange(), rule.getDstIp()); + final StaticNatRule staticNatRule = new StaticNatRule(rule.revoked(), rule.getProtocol(), rule.getSrcIp(), + rule.getStringSrcPortRange(), rule.getDstIp(), rule.isShouldApplyCrossNetworkSnat()); rules.add(staticNatRule); } final StaticNatRules staticNatRules = new StaticNatRules(rules); diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/StaticNatRule.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/StaticNatRule.java index a375a913b284..09f8daab0f76 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/StaticNatRule.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/StaticNatRule.java @@ -25,18 +25,25 @@ public class StaticNatRule { private String sourceIpAddress; private String sourcePortRange; private String destinationIpAddress; + private boolean shouldApplyCrossNetworkSnat = false; public StaticNatRule() { // Empty constructor for (de)serialization } public StaticNatRule(boolean revoke, String protocol, String sourceIpAddress, String sourcePortRange, String destinationIpAddress) { + this(revoke, protocol, sourceIpAddress, sourcePortRange, destinationIpAddress, false); + } + + public StaticNatRule(boolean revoke, String protocol, String sourceIpAddress, String sourcePortRange, + String destinationIpAddress, boolean shouldApplyCrossNetworkSnat) { super(); this.revoke = revoke; this.protocol = protocol; this.sourceIpAddress = sourceIpAddress; this.sourcePortRange = sourcePortRange; this.destinationIpAddress = destinationIpAddress; + this.shouldApplyCrossNetworkSnat = shouldApplyCrossNetworkSnat; } public boolean isRevoke() { @@ -79,4 +86,12 @@ public void setDestinationIpAddress(String destinationIpAddress) { this.destinationIpAddress = destinationIpAddress; } + public boolean isShouldApplyCrossNetworkSnat() { + return shouldApplyCrossNetworkSnat; + } + + public void setShouldApplyCrossNetworkSnat(boolean shouldApplyCrossNetworkSnat) { + this.shouldApplyCrossNetworkSnat = shouldApplyCrossNetworkSnat; + } + } diff --git a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java index c6296682bb09..b01083e6b078 100644 --- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java +++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java @@ -446,6 +446,7 @@ public void createApplyStaticNatRulesCommands(final List rules, final VirtualRouter router, final Commands cmds, final long guestNetworkId) { final List rulesTO = new ArrayList<>(); String systemRule = null; @@ -697,6 +717,7 @@ public void createApplyStaticNatCommands(final List rules, final IpAddress sourceIp = _networkModel.getIp(rule.getSourceIpAddressId()); final StaticNatRuleTO ruleTO = new StaticNatRuleTO(0, sourceIp.getAddress().addr(), null, null, rule.getDestIpAddress(), null, null, null, rule.isForRevoke(), false); + ruleTO.setShouldApplyCrossNetworkSnat(requiresReturnPathSnat(guestNetworkId, rule.getDestIpAddress())); rulesTO.add(ruleTO); } } diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java index 8ef77d3fb32f..58607b4a1067 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManager.java @@ -50,6 +50,7 @@ public interface VirtualNetworkApplianceManager extends Manager, VirtualNetworkA String RouterHealthChecksResultFetchIntervalCK = "router.health.checks.results.fetch.interval"; String RouterHealthChecksFailuresToRecreateVrCK = "router.health.checks.failures.to.recreate.vr"; String RemoveControlIpOnStopCK = "systemvm.release.control.ip.on.stop"; + String UseNicAwareSnat = "network.nic.snat.enabled"; ConfigKey RouterTemplateXen = new ConfigKey<>(String.class, RouterTemplateXenCK, "Advanced", "SystemVM Template (XenServer)", "Name of the default router template on Xenserver.", true, ConfigKey.Scope.Zone, null); @@ -131,6 +132,17 @@ public interface VirtualNetworkApplianceManager extends Manager, VirtualNetworkA ConfigKey RemoveControlIpOnStop = new ConfigKey<>(Boolean.class, RemoveControlIpOnStopCK, "Advanced", "true", "on stopping routers and system VMs the IP will be released to preserve IPv4 space.", true, ConfigKey.Scope.Zone, null); + ConfigKey NicSnatEnabled = new ConfigKey<>(Boolean.class, + UseNicAwareSnat, + "Advanced", + "false", + "When enabled, Virtual Router overwrites the SNAT source IP in POSTROUTING for traffic exiting " + + "non-default NICs. The SNAT source is selected based on the egress interface to preserve return path " + + "consistency in multi-NIC configurations.", + true, + ConfigKey.Scope.Global, + null); + int DEFAULT_ROUTER_VM_RAMSIZE = 256; // 256M int DEFAULT_ROUTER_CPU_MHZ = 500; // 500 MHz boolean USE_POD_VLAN = false; diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index dd65719ad033..f2207a34299a 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -3382,7 +3382,8 @@ public ConfigKey[] getConfigKeys() { ExposeDnsAndBootpServer, RouterLogrotateFrequency, RemoveControlIpOnStop, - VirtualRouterUserData + VirtualRouterUserData, + NicSnatEnabled }; } diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index bf48be66694c..0f4b96225d2f 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -1448,7 +1448,7 @@ def forward_vr(self, rule): ) fw4 = "-j SNAT --to-source %s -A POSTROUTING -s %s -d %s/32 -o %s -p %s -m %s --dport %s" % \ ( - self.getGuestIp(), + self.getGuestIpByIp(rule['internal_ip']), self.getNetworkByIp(rule['internal_ip']), rule['internal_ip'], internal_fwinterface, @@ -1563,11 +1563,20 @@ def processStaticNatRule(self, rule): self.fw.append(["filter", "", "-A FORWARD -i %s -o eth0 -d %s -m state --state NEW -j ACCEPT " % (device, rule["internal_ip"])]) - # Configure the hairpin snat - self.fw.append(["nat", "front", "-A POSTROUTING -s %s -d %s -j SNAT -o %s --to-source %s" % + # Configure the hairpin snat for default nic or nic-aware snat for non-default + apply_cross_network_snat = rule.get("should_apply_cross_network_snat", False) + if apply_cross_network_snat: + internal_device = self.getDeviceByIp(rule["internal_ip"]) + internal_vr_ip = self.getGuestIpByIp(rule["internal_ip"]) + if internal_device and internal_vr_ip and internal_device != device: + self.fw.append(["nat", "front", + "-A POSTROUTING -o %s -d %s/32 -j SNAT --to-source %s" % (internal_device, rule["internal_ip"], internal_vr_ip)]) + else: + self.fw.append(["nat", "front", "-A POSTROUTING -s %s -d %s -j SNAT -o %s --to-source %s" % (self.getNetworkByIp(rule['internal_ip']), rule["internal_ip"], self.getDeviceByIp(rule["internal_ip"]), self.getGuestIpByIp(rule["internal_ip"]))]) + class IpTablesExecutor: config = None diff --git a/systemvm/debian/opt/cloud/bin/cs_forwardingrules.py b/systemvm/debian/opt/cloud/bin/cs_forwardingrules.py index 199d4e77a988..81923387b3a1 100755 --- a/systemvm/debian/opt/cloud/bin/cs_forwardingrules.py +++ b/systemvm/debian/opt/cloud/bin/cs_forwardingrules.py @@ -27,6 +27,8 @@ def merge(dbag, rules): newrule = dict() newrule["public_ip"] = source_ip newrule["internal_ip"] = destination_ip + if "should_apply_cross_network_snat" in rule: + newrule["should_apply_cross_network_snat"] = rule["should_apply_cross_network_snat"] if rules["type"] == "staticnatrules": newrule["type"] = "staticnat"