Skip to content

Commit d2ddaf6

Browse files
committed
Direct Routed networks: drop the gateways entirely
The gateways on an L3 network were pure ballast: the Instance's gateway is always the shared link-local address (169.254.0.1 / fe80::1), the design doc already recorded the stored values as never used, and for IPv6 the declared gateway burnt an address in the subnet the operator then had to keep free. Stop requiring them: gateway and ip6gateway are accepted for API compatibility but ignored and stored as NULL. A complete IPv4 family is now netmask + startip (endip defaults to startip, the subnet derives from startip + netmask), a complete IPv6 family is ip6cidr alone; start/end IPs are validated against the subnet itself. The gateways were load-bearing in the plumbing though, in four places this rewires: - The orchestrator only copied cidr/ip6cidr onto the network when the matching gateway was present; L3 keeps its cidrs without one. - The two allocation gates keyed on gateway presence - IpAddressManagerImpl.allocateDirectIp() for IPv4 and Ipv6AddressManagerImpl.setNicIp6Address() for IPv6 - gate on the L3 network's cidr instead. - createVlanAndPublicIpRange() derived the range's subnet from the gateway and validated it; a gateway-less L3 range derives it from the start IP, and checkPublicIpRangeErrors()/checkZoneVlanIpOverlap() are null-gateway tolerant (the latter also stops keying its IPv6 overlap loop on ip6_gateway, using ip6_cidr - which is what it actually compares). - DirectRoutedNetworkGuru.design() no longer demands cidr and gateway as a pair. The UI form loses both gateway fields. Existing L3 networks with stored gateways keep working: every changed gate checks the gateway first. Tests: validator and checkL3Ip6Parameters unit tests reworked for the new groups, integration tests create gateway-less networks and assert given gateways are ignored, and the l3_network test data drops its gateway. Claude-Session: https://claude.ai/code/session_01LkswKyuC2a58YCHFTEPnay
1 parent 4546822 commit d2ddaf6

12 files changed

Lines changed: 180 additions & 105 deletions

File tree

docs/design/direct-routed-networks.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,17 @@ This is the mechanism Shared networks already use: `DirectNetworkGuru.allocateDi
271271
**Each family is optional (added 2026-09-09): IPv6-only networks are supported.** Nothing on an
272272
L3 network depends on IPv4 — no DHCP, no password or metadata service, and ConfigDrive carries
273273
whatever families exist — so network creation requires only that at least one family is given and
274-
that a given family is complete: IPv4 is gateway + netmask + startip (a pool is mandatory, since
275-
v4 addresses are drawn from one), IPv6 is ip6gateway + ip6cidr alone (no range — §6.3.4).
276-
Enforced by `NetworkServiceImpl.validateL3AddressFamilies()`; the allocation chain was already
277-
family-conditional and needed no change. IPv4-only networks work symmetrically.
274+
that a given family is complete: IPv4 is netmask + startip (a pool is mandatory, since v4
275+
addresses are drawn from one; the subnet derives from startip + netmask), IPv6 is ip6cidr alone
276+
(no range — §6.3.4). Enforced by `NetworkServiceImpl.validateL3AddressFamilies()`; the allocation
277+
chain gates on the cidrs. IPv4-only networks work symmetrically.
278+
279+
**Gateways play no part at all (revised 2026-09-09).** The Instance's gateway is always the
280+
shared link-local address (§6.2), so declaring a subnet gateway only burnt an address the
281+
operator then had to keep free. `gateway` and `ip6gateway` are accepted for API compatibility
282+
but ignored and stored as NULL; start/end IPs are validated against the subnet itself. The two
283+
allocation gates that historically keyed on the gateway (`allocateDirectIp()` for IPv4,
284+
`setNicIp6Address()` for IPv6) gate on the L3 network's cidr instead.
278285

279286
Consequences, all good:
280287

engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3119,12 +3119,14 @@ public Network doInTransaction(final TransactionStatus status) {
31193119
final NetworkVO userNetwork = new NetworkVO();
31203120
userNetwork.setNetworkDomain(networkDomainFinal);
31213121

3122-
if (cidr != null && gateway != null) {
3122+
// An L3 (Direct Routed) network carries no gateways at all: instances always
3123+
// use the shared link-local gateway, so the subnets stand on their own
3124+
if (cidr != null && (gateway != null || ntwkOff.getGuestType() == GuestType.L3)) {
31233125
userNetwork.setCidr(cidr);
31243126
userNetwork.setGateway(gateway);
31253127
}
31263128

3127-
if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) {
3129+
if (StringUtils.isNotBlank(ip6Cidr) && (StringUtils.isNotBlank(ip6Gateway) || ntwkOff.getGuestType() == GuestType.L3)) {
31283130
userNetwork.setIp6Cidr(ip6Cidr);
31293131
userNetwork.setIp6Gateway(ip6Gateway);
31303132
}

server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5841,9 +5841,12 @@ public Vlan createVlanAndPublicIpRange(final long zoneId, final long networkId,
58415841
throw new InvalidParameterValueException("Vlan owner can be defined only in the zone of type " + NetworkType.Advanced);
58425842
}
58435843

5844+
// An L3 (Direct Routed) range carries no gateways at all: the Instance's gateway is
5845+
// always the shared link-local address, and the IPv4 subnet stands on netmask + start IP
5846+
final boolean gatewaylessL3 = network.getGuestType() == GuestType.L3;
58445847
if (ipv4) {
58455848
// Make sure the gateway is valid
5846-
if (!NetUtils.isValidIp4(vlanGateway)) {
5849+
if (!(gatewaylessL3 && vlanGateway == null) && !NetUtils.isValidIp4(vlanGateway)) {
58475850
throw new InvalidParameterValueException("Please specify a valid gateway");
58485851
}
58495852

@@ -5854,7 +5857,7 @@ public Vlan createVlanAndPublicIpRange(final long zoneId, final long networkId,
58545857
}
58555858

58565859
if (ipv6) {
5857-
if (!NetUtils.isValidIp6(vlanIp6Gateway)) {
5860+
if (!(gatewaylessL3 && vlanIp6Gateway == null) && !NetUtils.isValidIp6(vlanIp6Gateway)) {
58585861
throw new InvalidParameterValueException("Please specify a valid IPv6 gateway");
58595862
}
58605863
if (!NetUtils.isValidIp6Cidr(vlanIp6Cidr)) {
@@ -5866,10 +5869,10 @@ public Vlan createVlanAndPublicIpRange(final long zoneId, final long networkId,
58665869
boolean isSharedNetworkWithoutSpecifyVlan = _networkMgr.isSharedNetworkWithoutSpecifyVlan(networkOffering);
58675870
boolean isL3NetworkWithoutSpecifyVlan = _networkMgr.isL3NetworkWithoutSpecifyVlan(networkOffering);
58685871
if (ipv4) {
5869-
final String newCidr = NetUtils.getCidrFromGatewayAndNetmask(vlanGateway, vlanNetmask);
5872+
final String newCidr = NetUtils.getCidrFromGatewayAndNetmask(vlanGateway != null ? vlanGateway : startIP, vlanNetmask);
58705873

58715874
//Make sure start and end ips are with in the range of cidr calculated for this gateway and netmask {
5872-
if (!NetUtils.isIpWithInCidrRange(vlanGateway, newCidr) || !NetUtils.isIpWithInCidrRange(startIP, newCidr) || !NetUtils.isIpWithInCidrRange(endIP, newCidr)) {
5875+
if ((vlanGateway != null && !NetUtils.isIpWithInCidrRange(vlanGateway, newCidr)) || !NetUtils.isIpWithInCidrRange(startIP, newCidr) || !NetUtils.isIpWithInCidrRange(endIP, newCidr)) {
58735876
throw new InvalidParameterValueException("Please specify a valid IP range or valid netmask or valid gateway");
58745877
}
58755878

@@ -5903,7 +5906,7 @@ public Vlan createVlanAndPublicIpRange(final long zoneId, final long networkId,
59035906

59045907
final List<VlanVO> vlans = _vlanDao.listByZone(zone.getId());
59055908
for (final VlanVO vlan : vlans) {
5906-
if (vlan.getIp6Gateway() == null) {
5909+
if (vlan.getIp6Cidr() == null) {
59075910
continue;
59085911
}
59095912
if ((StringUtils.isAllEmpty(ipv6Range, vlan.getIp6Range())) &&
@@ -6049,7 +6052,7 @@ private void checkZoneVlanIpOverlap(DataCenterVO zone, Network network, String n
60496052
}
60506053

60516054
// extend IP range
6052-
if (!vlanGateway.equals(otherVlanGateway) || !vlanNetmask.equals(vlan.getVlanNetmask())) {
6055+
if (!Objects.equals(vlanGateway, otherVlanGateway) || !Objects.equals(vlanNetmask, vlan.getVlanNetmask())) {
60536056
throw new InvalidParameterValueException("The IP range has already been added with gateway "
60546057
+ otherVlanGateway + " ,and netmask " + otherVlanNetmask
60556058
+ ", Please specify the gateway/netmask if you want to extend ip range" );
@@ -6853,24 +6856,30 @@ private void checkPublicIpRangeErrors(final long zoneId, final String vlanId, fi
68536856
throw new InvalidParameterValueException("Please ensure that your start IP and end IP are in the same subnet, as per the IP range's netmask.");
68546857
}
68556858

6856-
if (!NetUtils.sameSubnet(startIP, vlanGateway, vlanNetmask)) {
6857-
throw new InvalidParameterValueException("Please ensure that your start IP is in the same subnet as your IP range's gateway, as per the IP range's netmask.");
6858-
}
6859+
// A gateway-less range (L3 networks) skips the gateway checks: the subnet is defined by
6860+
// netmask + start IP alone
6861+
if (vlanGateway != null) {
6862+
if (!NetUtils.sameSubnet(startIP, vlanGateway, vlanNetmask)) {
6863+
throw new InvalidParameterValueException("Please ensure that your start IP is in the same subnet as your IP range's gateway, as per the IP range's netmask.");
6864+
}
68596865

6860-
if (endIP != null && !NetUtils.sameSubnet(endIP, vlanGateway, vlanNetmask)) {
6861-
throw new InvalidParameterValueException("Please ensure that your end IP is in the same subnet as your IP range's gateway, as per the IP range's netmask.");
6866+
if (endIP != null && !NetUtils.sameSubnet(endIP, vlanGateway, vlanNetmask)) {
6867+
throw new InvalidParameterValueException("Please ensure that your end IP is in the same subnet as your IP range's gateway, as per the IP range's netmask.");
6868+
}
68626869
}
68636870
// check if the gatewayip is the part of the ip range being added.
68646871
// RFC 3021 - 31-Bit Prefixes on IPv4 Point-to-Point Links
68656872
// GW Netmask Stat IP End IP
68666873
// 192.168.24.0 - 255.255.255.254 - 192.168.24.0 - 192.168.24.1
68676874
// https://tools.ietf.org/html/rfc3021
68686875
// Added by Wilder Rodrigues
6869-
final String newCidr = NetUtils.getCidrFromGatewayAndNetmask(vlanGateway, vlanNetmask);
6870-
if (!NetUtils.is31PrefixCidr(newCidr)) {
6871-
if (NetUtils.ipRangesOverlap(startIP, endIP, vlanGateway, vlanGateway)) {
6872-
throw new InvalidParameterValueException(
6873-
"The gateway ip should not be the part of the ip range being added.");
6876+
if (vlanGateway != null) {
6877+
final String newCidr = NetUtils.getCidrFromGatewayAndNetmask(vlanGateway, vlanNetmask);
6878+
if (!NetUtils.is31PrefixCidr(newCidr)) {
6879+
if (NetUtils.ipRangesOverlap(startIP, endIP, vlanGateway, vlanGateway)) {
6880+
throw new InvalidParameterValueException(
6881+
"The gateway ip should not be the part of the ip range being added.");
6882+
}
68746883
}
68756884
}
68766885
}

server/src/main/java/com/cloud/network/IpAddressManagerImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2436,9 +2436,9 @@ public void allocateDirectIp(final NicProfile nic, final DataCenter dc, final Vi
24362436
Transaction.execute(new TransactionCallbackWithExceptionNoReturn<InsufficientAddressCapacityException>() {
24372437
@Override
24382438
public void doInTransactionWithoutResult(TransactionStatus status) throws InsufficientAddressCapacityException {
2439-
//This method allocates direct ip for the Shared network in Advance zones
2440-
boolean ipv4 = false;
2441-
if (network.getGateway() != null) {
2439+
//This method allocates direct ip for the Shared network in Advance zones.
2440+
//An L3 (Direct Routed) network carries no gateway, so its IPv4 presence is the cidr
2441+
if (network.getGateway() != null || (GuestType.L3 == network.getGuestType() && network.getCidr() != null)) {
24422442
if (nic.getIPv4Address() == null) {
24432443
PublicIp ip = null;
24442444

server/src/main/java/com/cloud/network/Ipv6AddressManagerImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ protected boolean isIp6Taken(Network network, String requestedIpv6) {
204204
*/
205205
@Override
206206
public void setNicIp6Address(final NicProfile nic, final DataCenter dc, final Network network) throws InsufficientAddressCapacityException {
207-
if (network.getIp6Gateway() != null) {
207+
// An L3 (Direct Routed) network carries no gateway, so its IPv6 presence is the cidr
208+
if (network.getIp6Gateway() != null || (Network.GuestType.L3 == network.getGuestType() && network.getIp6Cidr() != null)) {
208209
if (nic.getIPv6Address() == null) {
209210
logger.debug("Found IPv6 CIDR " + network.getIp6Cidr() + " for Network " + network);
210211
nic.setIPv6Cidr(network.getIp6Cidr());

server/src/main/java/com/cloud/network/NetworkServiceImpl.java

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -875,28 +875,51 @@ protected NetworkServiceImpl() {
875875
/**
876876
* An L3 (Direct Routed) network has no DHCP and no password/metadata service, so nothing in
877877
* it depends on IPv4: each address family is optional, making IPv6-only networks possible.
878-
* What remains mandatory is that a given family is complete — IPv4 is gateway, netmask and
879-
* startip (endip defaults to startip), IPv6 is ip6gateway and ip6cidr (no range: addresses
878+
* Gateways play no part at all — the Instance's gateway is always the shared link-local
879+
* address, so none needs to be declared (or burnt in the subnet). What remains mandatory is
880+
* that a given family is complete — IPv4 is netmask and startip (endip defaults to startip,
881+
* the subnet derives from startip and netmask), IPv6 is ip6cidr alone (no range: addresses
880882
* derive from the subnet and the NIC MAC with EUI-64) — and that at least one family is
881883
* present at all.
882884
*/
883-
protected void validateL3AddressFamilies(String gateway, String netmask, String startIP, String endIP,
884-
String ip6Gateway, String ip6Cidr, String startIPv6, String endIPv6) {
885-
boolean anyIpv4 = !StringUtils.isAllBlank(gateway, netmask, startIP, endIP);
886-
boolean completeIpv4 = StringUtils.isNoneBlank(gateway, netmask, startIP);
887-
boolean anyIpv6 = !StringUtils.isAllBlank(ip6Gateway, ip6Cidr, startIPv6, endIPv6);
888-
boolean completeIpv6 = StringUtils.isNoneBlank(ip6Gateway, ip6Cidr);
885+
protected void validateL3AddressFamilies(String netmask, String startIP, String endIP,
886+
String ip6Cidr, String startIPv6, String endIPv6) {
887+
boolean anyIpv4 = !StringUtils.isAllBlank(netmask, startIP, endIP);
888+
boolean completeIpv4 = StringUtils.isNoneBlank(netmask, startIP);
889+
boolean anyIpv6 = !StringUtils.isAllBlank(ip6Cidr, startIPv6, endIPv6);
890+
boolean completeIpv6 = StringUtils.isNotBlank(ip6Cidr);
889891
if (anyIpv4 && !completeIpv4) {
890892
throw new InvalidParameterValueException(String.format(
891-
"IPv4 is optional for %s networks, but when any IPv4 detail is given, gateway, netmask and startip are all required", GuestType.L3));
893+
"IPv4 is optional for %s networks, but when any IPv4 detail is given, netmask and startip are both required", GuestType.L3));
892894
}
893895
if (anyIpv6 && !completeIpv6) {
894896
throw new InvalidParameterValueException(String.format(
895-
"IPv6 is optional for %s networks, but when any IPv6 detail is given, ip6gateway and ip6cidr are both required", GuestType.L3));
897+
"IPv6 is optional for %s networks, but when any IPv6 detail is given, ip6cidr is required", GuestType.L3));
896898
}
897899
if (!anyIpv4 && !anyIpv6) {
898900
throw new InvalidParameterValueException(String.format(
899-
"A %s network needs at least one address family: IPv4 (gateway, netmask, startip) or IPv6 (ip6gateway, ip6cidr)", GuestType.L3));
901+
"A %s network needs at least one address family: IPv4 (netmask, startip) or IPv6 (ip6cidr)", GuestType.L3));
902+
}
903+
}
904+
905+
/**
906+
* The L3 counterpart of {@code NetworkModel.checkIp6Parameters()}: no gateway is involved,
907+
* and the range is optional — when given it must lie inside the subnet.
908+
*/
909+
protected void checkL3Ip6Parameters(String startIPv6, String endIPv6, String ip6Cidr) {
910+
if (!NetUtils.isValidIp6Cidr(ip6Cidr)) {
911+
throw new InvalidParameterValueException("Invalid ip6cidr");
912+
}
913+
for (String ip : new String[] {startIPv6, endIPv6}) {
914+
if (StringUtils.isBlank(ip)) {
915+
continue;
916+
}
917+
if (!NetUtils.isValidIp6(ip)) {
918+
throw new InvalidParameterValueException(String.format("Invalid IPv6 address %s", ip));
919+
}
920+
if (!NetUtils.isIp6InNetwork(ip, ip6Cidr)) {
921+
throw new InvalidParameterValueException(String.format("IPv6 address %s is not within the subnet %s", ip, ip6Cidr));
922+
}
900923
}
901924
}
902925

@@ -1634,12 +1657,29 @@ public Network createGuestNetwork(CreateNetworkCmd cmd) throws InsufficientCapac
16341657
}
16351658
}
16361659

1637-
boolean ipv4 = false, ipv6 = false;
1638-
if (org.apache.commons.lang3.StringUtils.isNoneBlank(gateway, netmask)) {
1639-
ipv4 = true;
1660+
final boolean isL3 = ntwkOff.getGuestType() == GuestType.L3;
1661+
if (isL3) {
1662+
validateL3AddressFamilies(netmask, startIP, endIP, ip6Cidr, startIPv6, endIPv6);
1663+
// Gateways are ignored on L3 networks: the Instance's gateway is always the shared
1664+
// link-local address, so requiring one would only burn an address in the subnet
1665+
if (!StringUtils.isAllBlank(gateway, ip6Gateway)) {
1666+
logger.debug("Ignoring the gateway(s) given for {} network {}: instances always use the shared link-local gateway", GuestType.L3, name);
1667+
gateway = null;
1668+
ip6Gateway = null;
1669+
}
16401670
}
1641-
if (StringUtils.isNoneBlank(ip6Cidr, ip6Gateway)) {
1642-
ipv6 = true;
1671+
1672+
boolean ipv4 = false, ipv6 = false;
1673+
if (isL3) {
1674+
ipv4 = StringUtils.isNoneBlank(netmask, startIP);
1675+
ipv6 = StringUtils.isNotBlank(ip6Cidr);
1676+
} else {
1677+
if (org.apache.commons.lang3.StringUtils.isNoneBlank(gateway, netmask)) {
1678+
ipv4 = true;
1679+
}
1680+
if (StringUtils.isNoneBlank(ip6Cidr, ip6Gateway)) {
1681+
ipv6 = true;
1682+
}
16431683
}
16441684

16451685
if (gateway != null) {
@@ -1666,9 +1706,6 @@ public Network createGuestNetwork(CreateNetworkCmd cmd) throws InsufficientCapac
16661706
throw new InvalidParameterValueException("Either IPv4 or IPv6 start and end address are mandatory");
16671707
}
16681708
}
1669-
if (ntwkOff.getGuestType() == GuestType.L3) {
1670-
validateL3AddressFamilies(gateway, netmask, startIP, endIP, ip6Gateway, ip6Cidr, startIPv6, endIPv6);
1671-
}
16721709

16731710
String cidr = null;
16741711
if (ipv4) {
@@ -1682,11 +1719,17 @@ public Network createGuestNetwork(CreateNetworkCmd cmd) throws InsufficientCapac
16821719
} else if (!NetUtils.isValidIp4(endIP)) {
16831720
throw new InvalidParameterValueException("Invalid format for the endIp parameter");
16841721
}
1685-
if (!(gateway != null && netmask != null)) {
1722+
if (!(netmask != null && (gateway != null || isL3))) {
16861723
throw new InvalidParameterValueException("gateway and netmask should be defined when startIP/endIP are passed in");
16871724
}
16881725
}
1689-
if (gateway != null && netmask != null) {
1726+
if (isL3 && netmask != null && startIP != null) {
1727+
if (!NetUtils.isValidIp4Netmask(netmask)) {
1728+
throw new InvalidParameterValueException("Invalid netmask");
1729+
}
1730+
// No gateway on an L3 network: the subnet derives from the start IP instead
1731+
cidr = NetUtils.getCidrFromGatewayAndNetmask(startIP, netmask);
1732+
} else if (gateway != null && netmask != null) {
16901733
if (NetUtils.isNetworkorBroadcastIP(gateway, netmask)) {
16911734
if (logger.isDebugEnabled()) {
16921735
logger.debug("The gateway IP provided is " + gateway + " and netmask is " + netmask + ". The IP is either broadcast or network IP.");
@@ -1710,7 +1753,11 @@ public Network createGuestNetwork(CreateNetworkCmd cmd) throws InsufficientCapac
17101753
if (endIPv6 == null) {
17111754
endIPv6 = startIPv6;
17121755
}
1713-
_networkModel.checkIp6Parameters(startIPv6, endIPv6, ip6Gateway, ip6Cidr);
1756+
if (isL3) {
1757+
checkL3Ip6Parameters(startIPv6, endIPv6, ip6Cidr);
1758+
} else {
1759+
_networkModel.checkIp6Parameters(startIPv6, endIPv6, ip6Gateway, ip6Cidr);
1760+
}
17141761
if (!GuestType.Shared.equals(ntwkOff.getGuestType()) && !GuestType.L3.equals(ntwkOff.getGuestType())) {
17151762
_networkModel.checkIp6CidrSizeEqualTo64(ip6Cidr);
17161763
}

0 commit comments

Comments
 (0)