File: SubnetAddressAllocator.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.Sql\Aspire.Hosting.Azure.Sql.csproj (Aspire.Hosting.Azure.Sql)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Sockets;
using Aspire.Hosting.Azure;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Allocates subnet address space within a virtual network.
/// </summary>
[Experimental("ASPIREAZURE003")]
internal static class SubnetAddressAllocator
{
    /// <summary>
    /// Allocates a /29 subnet from the highest available address in the virtual network.
    /// </summary>
    /// <param name="vnet">The virtual network to allocate from.</param>
    /// <param name="existingSubnets">All existing subnets in the VirtualNetwork (from the app model).</param>
    /// <returns>The CIDR notation for the allocated subnet (e.g., "10.0.255.248/29").</returns>
    /// <exception cref="InvalidOperationException">
    /// Thrown when the VirtualNetwork address prefix is parameterized, or when no space is available.
    /// </exception>
    public static string AllocateDeploymentScriptSubnet(AzureVirtualNetworkResource vnet, IEnumerable<AzureSubnetResource> existingSubnets)
    {
        const int prefixLength = 29;
        const uint blockSize = 8; // 2^(32-29) = 8
 
        var vnetAddressPrefix = vnet.AddressPrefix
            ?? throw new InvalidOperationException(
                $"Cannot automatically allocate a deployment script subnet for virtual network '{vnet.Name}' because it uses a parameterized address prefix. " +
                $"Use 'WithAdminDeploymentScriptSubnet' to provide an explicit subnet.");
 
        var (vnetStart, vnetEnd) = ParseCidr(vnetAddressPrefix);
 
        // Ensure the VNet is large enough to contain at least one /29 block
        if (vnetEnd - vnetStart + 1 < blockSize)
        {
            throw new InvalidOperationException(
                $"Virtual network '{vnet.Name}' address space '{vnetAddressPrefix}' is too small to allocate a /{prefixLength} subnet (requires at least {blockSize} addresses). " +
                $"Use 'WithAdminDeploymentScriptSubnet' to provide an explicit subnet.");
        }
 
        // Collect all existing subnet ranges
        var existingRanges = new List<(uint Start, uint End)>();
        foreach (var subnet in existingSubnets)
        {
            if (subnet.AddressPrefix is { } subnetCidr)
            {
                var range = ParseCidr(subnetCidr);
                existingRanges.Add(range);
            }
            // Skip subnets with parameterized addresses — can't check overlap
        }
 
        // Start from the highest /29-aligned address and work downward
        var candidate = (vnetEnd - blockSize + 1) & ~(blockSize - 1);
 
        while (candidate >= vnetStart)
        {
            var candidateEnd = candidate + blockSize - 1;
 
            if (candidateEnd <= vnetEnd && !OverlapsAny(candidate, candidateEnd, existingRanges))
            {
                return $"{UintToIp(candidate)}/{prefixLength}";
            }
 
            if (candidate < blockSize)
            {
                break; // Prevent underflow
            }
 
            candidate -= blockSize;
        }
 
        throw new InvalidOperationException(
            $"Cannot allocate a /29 subnet in virtual network '{vnet.Name}' (address space: {vnetAddressPrefix}). " +
            $"No non-overlapping address space is available. " +
            $"Use 'WithAdminDeploymentScriptSubnet' to provide an explicit subnet.");
    }
 
    private static bool OverlapsAny(uint start, uint end, List<(uint Start, uint End)> ranges)
    {
        foreach (var (rStart, rEnd) in ranges)
        {
            if (start <= rEnd && rStart <= end)
            {
                return true;
            }
        }
 
        return false;
    }
 
    internal static (uint Start, uint End) ParseCidr(string cidr)
    {
        var parts = cidr.Split('/');
        if (parts.Length != 2 || !int.TryParse(parts[1], out var prefix) || prefix < 0 || prefix > 32)
        {
            throw new FormatException($"Invalid CIDR notation: '{cidr}'.");
        }
 
        var ip = IPAddress.Parse(parts[0]);
        if (ip.AddressFamily != AddressFamily.InterNetwork)
        {
            throw new FormatException($"Only IPv4 CIDR notation is supported: '{cidr}'.");
        }
 
        var bytes = ip.GetAddressBytes();
        var address = (uint)((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
 
        // Compute the network mask
        var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
        var networkAddress = address & mask;
        var broadcastAddress = networkAddress | ~mask;
 
        return (networkAddress, broadcastAddress);
    }
 
    private static string UintToIp(uint address)
    {
        return $"{(address >> 24) & 0xFF}.{(address >> 16) & 0xFF}.{(address >> 8) & 0xFF}.{address & 0xFF}";
    }
}