Skip to content

Commit 0cc8922

Browse files
committed
Direct Routed (L3) guest networks: route public IPv4/IPv6 directly to Instances
Add a guest network type in which the hypervisor performs L3 routing for the Instance: no Virtual Router, no NAT and no DHCP. Each Instance receives a public IPv4 address as a /32 and/or an IPv6 address as a /128, with a shared, host-independent link-local gateway (169.254.0.1 and fe80::1) that every hypervisor carries. All addressing reaches the Instance exclusively via ConfigDrive/cloud-init; a routing daemon on the host (FRR, BIRD, ...) advertises the addresses to the fabric and is deliberately out of scope for CloudStack. The networks live on a dedicated physical network with the new ROUTED isolation method, so the feature can be added to existing zones without touching anything already running there. Each network carries a routed://<id> broadcast domain - the id allocated from the physical network's vnet range or chosen by the operator - naming the network's uplink-less bridge (brdr-<id>) that the new modifybrdr.sh manages on every host: gateway addresses, forwarding sysctls, strict rp_filter, and a MAC derived from the routed id so it is identical on every hypervisor and live migration never invalidates the guest's neighbour cache. The existing modifymacip.sh installs the per-address host route and static neighbour entry when a NIC is plugged. Both address families are optional and IPv6-only networks are supported: IPv4 is a subnet (cidr=..., or netmask + start/end IP), IPv6 is an ip6cidr alone - addresses derive from the subnet and the NIC MAC with EUI-64, so no range exists. No gateways are declared or stored; createNetwork gains an optional cidr parameter (L3-only, additive). Security groups work through a --directrouted dispatch in security_group.py that matches the return path by ipset destination instead of bridge port; a golden-file harness pins the classic rule stream to prove existing deployments unchanged. SystemVMs run on routed public ranges (createVlanIpRange with vlan=routed://<id>) in the same host-route form. ConfigDrive network data is always generated for these NICs, with the IPv4 default route emitted as a network-level gateway key so cloud-init (netplan >= 23.1, networkd >= 24.2) renders it on-link. A DefaultL3NetworkOffering (UserData and DNS via ConfigDrive, security groups) is created on install and upgrade, and the UI gains an L3 creation form, ROUTED in the zone wizard, and active IPv4/IPv6 addresses in the Instance list with click-to-copy. General fixes that the feature surfaced but apply beyond it: auto-allocated ids are exempt from the dynamic-vlan-range check in createVlanAndPublicIpRange; a failed VM start or migration prepare now unplugs the NICs it plugged; canUseForDeploy() counts the real IPv4 pool and no longer hides IPv4-less networks from the deploy wizard; the zone-wide IPv6 overlap check keys on ip6_cidr rather than ip6_gateway. The design document with the decision log lives in docs/design/. This implements #12210 Claude-Session: https://claude.ai/code/session_01LkswKyuC2a58YCHFTEPnay
1 parent a723d44 commit 0cc8922

57 files changed

Lines changed: 5010 additions & 262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/src/main/java/com/cloud/network/Network.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
public interface Network extends ControlledEntity, StateObject<Network.State>, InternalIdentity, Identity, Serializable, Displayable {
4444

4545
enum GuestType {
46-
Shared, Isolated, L2;
46+
Shared, Isolated, L2, L3;
4747

4848
public static GuestType fromValue(String type) {
4949
if (StringUtils.isBlank(type)) {
@@ -54,6 +54,8 @@ public static GuestType fromValue(String type) {
5454
return Isolated;
5555
} else if (type.equalsIgnoreCase("L2")) {
5656
return L2;
57+
} else if (type.equalsIgnoreCase("L3")) {
58+
return L3;
5759
} else {
5860
throw new InvalidParameterValueException("Unexpected Guest type : " + type);
5961
}

api/src/main/java/com/cloud/network/Networks.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,23 @@ public <T> URI toUri(T value) {
131131
}
132132
}
133133
},
134+
/**
135+
* Direct Routed (L3) networks: the id is a label naming the per-network bridge on the
136+
* hypervisor (brdr-&lt;id&gt;), not an encapsulation — nothing appears on the wire.
137+
*/
138+
Routed("routed", Long.class) {
139+
@Override
140+
public <T> URI toUri(T value) {
141+
try {
142+
if (value.toString().contains("://"))
143+
return new URI(value.toString());
144+
else
145+
return new URI("routed://" + value.toString());
146+
} catch (URISyntaxException e) {
147+
throw new CloudRuntimeException("Unable to convert to broadcast URI: " + value);
148+
}
149+
}
150+
},
134151
UnDecided(null, null),
135152
OpenDaylight("opendaylight", String.class),
136153
TUNGSTEN("tf", String.class),

api/src/main/java/com/cloud/offering/NetworkOffering.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ enum RoutingMode {
8181
public final static String DefaultL2NetworkOfferingVlan = "DefaultL2NetworkOfferingVlan";
8282
public final static String DefaultL2NetworkOfferingConfigDrive = "DefaultL2NetworkOfferingConfigDrive";
8383
public final static String DefaultL2NetworkOfferingConfigDriveVlan = "DefaultL2NetworkOfferingConfigDriveVlan";
84+
public final static String DefaultL3NetworkOffering = "DefaultL3NetworkOffering";
8485

8586
/**
8687
* @return name for the network offering.

api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ public class CreateNetworkCmd extends BaseCmd implements UserCmd {
8787
+ "for shared networks and isolated networks when it belongs to VPC")
8888
private String netmask;
8989

90+
@Parameter(name = ApiConstants.CIDR, type = CommandType.STRING, since = "24.0.0",
91+
description = "The IPv4 subnet of the Network in CIDR notation, e.g. 192.0.2.0/24. Supported for L3 (Direct Routed) "
92+
+ "networks only, as an alternative to netmask: the IP range defaults to the subnet's usable addresses, "
93+
+ "and startip/endip may narrow it")
94+
private String cidr;
95+
9096
@Parameter(name = ApiConstants.START_IP, type = CommandType.STRING, description = "The beginning IP address in the Network IP range")
9197
private String startIp;
9298

@@ -231,6 +237,10 @@ public String getNetmask() {
231237
return netmask;
232238
}
233239

240+
public String getCidr() {
241+
return cidr;
242+
}
243+
234244
public String getStartIp() {
235245
return startIp;
236246
}
@@ -340,10 +350,10 @@ public Long getPhysicalNetworkId() {
340350
}
341351
}
342352
if (physicalNetworkId != null) {
343-
if ((offering.getGuestType() == GuestType.Shared) || (offering.getGuestType() == GuestType.L2)) {
353+
if ((offering.getGuestType() == GuestType.Shared) || (offering.getGuestType() == GuestType.L2) || (offering.getGuestType() == GuestType.L3)) {
344354
return physicalNetworkId;
345355
} else {
346-
throw new InvalidParameterValueException("Physical network ID can be specified for networks of guest IP type " + GuestType.Shared + " or " + GuestType.L2 + " only.");
356+
throw new InvalidParameterValueException(String.format("Physical network ID can be specified for networks of guest IP type %s, %s or %s only.", GuestType.Shared, GuestType.L2, GuestType.L3));
347357
}
348358
} else {
349359
if (zoneId == null) {

api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveIpFromVmNicCmd.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ public NetworkType getNetworkType() {
124124
}
125125

126126

127+
/**
128+
* A Direct Routed (L3) network needs the agent told when a secondary IP goes away, so the
129+
* host route and neighbour entry are removed - otherwise the host keeps routing an address
130+
* the Instance no longer owns, and the routing daemon keeps advertising it.
131+
*/
132+
private boolean isDirectRoutedNetwork() {
133+
Network ntwk = _entityMgr.findById(Network.class, getNetworkId());
134+
return ntwk != null && Network.GuestType.L3.equals(ntwk.getGuestType());
135+
}
136+
127137
private boolean isZoneSGEnabled() {
128138
Network ntwk = _entityMgr.findById(Network.class, getNetworkId());
129139
DataCenter dc = _entityMgr.findById(DataCenter.class, ntwk.getDataCenterId());
@@ -144,7 +154,7 @@ public void execute() throws InvalidParameterValueException {
144154
secIp = nicSecIp.getIp6Address();
145155
}
146156

147-
if (isZoneSGEnabled()) {
157+
if (isZoneSGEnabled() || isDirectRoutedNetwork()) {
148158
//remove the security group rules for this secondary ip
149159
boolean success = false;
150160
success = _securityGroupService.securityGroupRulesForVmSecIp(nicSecIp.getNicId(), secIp, false);

api/src/test/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmdTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ public void testGetPhysicalNetworkIdForNonSharedNet() {
251251
try {
252252
cmd.getPhysicalNetworkId();
253253
} catch (Exception e) {
254-
Assert.assertTrue(e.getMessage().startsWith("Physical network ID can be specified for networks of guest IP type Shared or L2 only."));
254+
Assert.assertTrue(e.getMessage().startsWith("Physical network ID can be specified for networks of guest IP type Shared, L2 or L3 only."));
255255
}
256256
}
257257

core/src/main/java/com/cloud/agent/api/NetworkRulesVmSecondaryIpCommand.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,28 @@ public class NetworkRulesVmSecondaryIpCommand extends Command {
2828
private String vmSecIp;
2929
private String vmMac;
3030
private String action;
31+
private boolean directRouted;
32+
private boolean applySecurityGroupRules = true;
3133

3234
public NetworkRulesVmSecondaryIpCommand(String vmName, VirtualMachine.Type type) {
3335
this.vmName = vmName;
3436
this.type = type;
3537
}
3638

39+
public NetworkRulesVmSecondaryIpCommand(String vmName, String vmMac, String secondaryIp, boolean action, boolean directRouted, boolean applySecurityGroupRules) {
40+
this(vmName, vmMac, secondaryIp, action);
41+
this.directRouted = directRouted;
42+
this.applySecurityGroupRules = applySecurityGroupRules;
43+
}
44+
45+
public boolean isDirectRouted() {
46+
return directRouted;
47+
}
48+
49+
public boolean isApplySecurityGroupRules() {
50+
return applySecurityGroupRules;
51+
}
52+
3753
public NetworkRulesVmSecondaryIpCommand(String vmName, String vmMac, String secondaryIp, boolean action) {
3854
this.vmName = vmName;
3955
this.vmMac = vmMac;

0 commit comments

Comments
 (0)