Skip to content

Routed VPC / network CIDR uniqueness is enforced globally instead of per zone #14419

Open

Description

@prashanthr2

problem

For routed networks and routed VPCs, CloudStack treats a guest CIDR as unique across the entire installation rather than per zone. Creating a routed VPC with CIDR 10.0.0.0/8 in zone A and then the same CIDR in zone B fails with The subnet is in use, even though the two zones are independent L3 domains with separate fabrics.

Routed networks are inherently zone-scoped , they are tied to that zone’s routing fabric, and the parent table dc_ip4_guest_subnets is explicitly per-zone via data_center_id. The uniqueness check therefore looks over-strict rather than intentional, and it blocks a common active/passive DR pattern where the standby zone deliberately uses the same addressing as the primary.

versions

4.22.1

The steps to reproduce the bug

  1. Enable routed networks/VPCs. Do not configure any zone IPv4 guest subnets.
  2. As root admin, create a routed VPC in zone A with cidr=10.0.0.0/8 -> succeeds
  3. As root admin, create a routed VPC in zone B with cidr=10.0.0.0/8 -> fails (Request failed (431): The subnet is in use)

What to do about it?

RoutedIpv4ManagerImpl.getOrCreateIpv4SubnetForGuestNetworkOrVpcInternal(String networkCidr, ...) (line 652 in 4.22.1.0) performs an exact-match lookup with no zone qualifier:

Ipv4GuestSubnetNetworkMapVO subnetMap = ipv4GuestSubnetNetworkMapDao.findBySubnet(networkCidr);
if (subnetMap != null) {
    // check if the subnet is in use
    if (subnetMap.getNetworkId() != null || subnetMap.getVpcId() != null) {
        throw new InvalidParameterValueException("The subnet is in use");
    }

Ipv4GuestSubnetNetworkMapDaoImpl.findBySubnet() (line 145) uses a search builder with only a subnet predicate (line 71):

SubnetSearch = createSearchBuilder();
SubnetSearch.and("subnet", SubnetSearch.entity().getSubnet(), SearchCriteria.Op.EQ);
SubnetSearch.done();

The underlying table has no zone column at all — Ipv4GuestSubnetNetworkMapVO (@table(name = "ip4_guest_subnet_network_map")) exposes only id, uuid, parent_id, subnet, vpc_id, network_id, state, allocated, created, removed. Zone is reachable only indirectly through parent_id → dc_ip4_guest_subnets.data_center_id.

This makes the registry internally inconsistent: rows with a parent are implicitly zone-scoped, while parentless rows carry no zone information whatsoever and therefore block their CIDR installation-wide. Parentless rows are the normal case when no zone subnets are configured — getParentOfNetworkCidr() returns null and the row is created with parentId = null.

Both entry points funnel into the same method, so routed VPCs and plain routed isolated networks are equally affected:

  • getOrCreateIpv4SubnetForVpc(Vpc, String)
  • getOrCreateIpv4SubnetForGuestNetwork(Network, String)

Evidence from a reproduction — one parentless row blocking the CIDR globally:

mysql> SELECT m.id, m.subnet, m.state, m.parent_id, m.network_id, m.vpc_id,
              v.name AS vpc_name, v.zone_id AS vpc_zone
       FROM cloud.ip4_guest_subnet_network_map m
       LEFT JOIN cloud.vpc v ON v.id = m.vpc_id
       WHERE m.subnet = '10.0.0.0/8';
+----+------------+-----------+-----------+------------+--------+-----------+----------+
| id | subnet     | state     | parent_id | network_id | vpc_id | vpc_name  | vpc_zone |
+----+------------+-----------+-----------+------------+--------+-----------+----------+
|  3 | 10.0.0.0/8 | Allocated |      NULL |       NULL |      5 | RouteVPC2 |        2 |
+----+------------+-----------+-----------+------------+--------+-----------+----------+

For contrast, non-routed VPCs have no CIDR uniqueness check at all — VpcManagerImpl.createVpc() validates only that the CIDR is a valid IPv4 CIDR and RFC1918-compliant — so the same CIDR in two zones works there. The restriction is specific to the routed path.

SUGGESTED FIX

Scope the lookup to the zone:

  1. Add a data_center_id column to ip4_guest_subnet_network_map, populated on creation. This is required because parentless rows have no other path to a zone.
  2. Add a zone predicate to SubnetSearch / findBySubnet() and pass zoneId from the callers (already available in getOrCreateIpv4SubnetForGuestNetworkOrVpcInternal).
  3. Apply the same scoping to the overlap check in getParentOfNetworkCidr(), which uses listAllNoParent() and is likewise installation-wide.
  4. Upgrade: backfill data_center_id from vpc.zone_id / networks.data_center_id for allocated rows, and from dc_ip4_guest_subnets.data_center_id where a parent exists.

WORKAROUNDS

  1. Use a non-routed (NAT) VPC offering in the second zone — identical CIDRs work today.
  2. Use different CIDRs per zone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions