Search code examples
c#asp.net-corekubernetes

How to automatically determine KnownNetworks for ASP.NET Core application running in Kubernetes with an in-cluster reverse proxy?


I'm running an ASP.NET Core API in Kubernetes behind a reverse proxy that's sending X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host headers.

I've found that I need to use UseForwardedHeaders() to accept the values from the proxy, so I've written the following code:

var forwardedOptions = new ForwardedHeadersOptions()
{
    ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All
};
forwardedOptions.KnownNetworks.Add(new IPNetwork(IPAddress.Parse(configuration["network:address"]), int.Parse(configuration["network:cidrMask"])));
app.UseForwardedHeaders(forwardedOptions);

I'm running my API and reverse proxy within Kubernetes, and the API is only visible in the cluster. Because of this, I'm not worried about somebody on the cluster network spoofing the headers. What I would like to do is to automatically detect the cluster's internal subnet and add this to the KnownNetworks list. Is this possible? If so, how?


Solution

  • I've created a method that calculates the start in the range and the CIDR subnet mask for each active interface:

    private static IEnumerable<IPNetwork> GetNetworks(NetworkInterfaceType type)
    {
    
        foreach (var item in NetworkInterface.GetAllNetworkInterfaces()
            .Where(n => n.NetworkInterfaceType == type && n.OperationalStatus == OperationalStatus.Up)  // get all operational networks of a given type
            .Select(n => n.GetIPProperties())   // get the IPs
            .Where(n => n.GatewayAddresses.Any())) // where the IPs have a gateway defined
        {
            var ipInfo = item.UnicastAddresses.FirstOrDefault(i => i.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); // get the first cluster-facing IP address
            if (ipInfo == null) { continue; }
    
            // convert the mask to bits
            var maskBytes = ipInfo.IPv4Mask.GetAddressBytes();
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(maskBytes);
            }
            var maskBits = new BitArray(maskBytes);
    
            // count the number of "true" bits to get the CIDR mask
            var cidrMask = maskBits.Cast<bool>().Count(b => b); 
    
            // convert my application's ip address to bits
            var ipBytes = ipInfo.Address.GetAddressBytes();
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(ipBytes);
            }
            var ipBits = new BitArray(ipBytes);
    
            // and the bits with the mask to get the start of the range
            var maskedBits = ipBits.And(maskBits);
            
            // Convert the masked IP back into an IP address
            var maskedIpBytes = new byte[4];
            maskedBits.CopyTo(maskedIpBytes, 0);
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(maskedIpBytes);
            }
            var rangeStartIp = new IPAddress(maskedIpBytes);
    
            // return the start IP and CIDR mask
            yield return new IPNetwork(rangeStartIp, cidrMask);
        }
    }
    

    Examples:

    • 192.168.1.33 with mask 255.255.255.252 returns 192.168.1.32/30
    • 10.50.28.77 with mask 255.252.0.0 returns 10.50.0.0/14

    I've then changed my options code to look like this:

    var forwardedOptions = new ForwardedHeadersOptions()
    {
        ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All
    };
    foreach (var network in GetNetworks(NetworkInterfaceType.Ethernet))
    {
        forwardedOptions.KnownNetworks.Add(network);
    }
    app.UseForwardedHeaders(forwardedOptions);