How to call ICostManager::GetCost from C#

Windows 8 introduced the concept of a metered connection. A metered connection indicates that the user is being charged for data transfer on that network, and so you might want to limit how much you transfer. WinRT provides classes in the Windows.Networking.Connectivity namespace to determine whether or not a given connection is metered (and much more besides), but there’s nothing directly available in traditional .NET applications.

You can however access the INetworkCostManager directly over COM: add a COM reference to the ‘Network List Manager 1.0 Type Library’, and you can access functions like INetworkCostManager::GetCost. Problem solved?

Not quite.

INetworkCostManager::GetCost has the signature:

HRESULT GetCost(
  [out]        DWORD        *pCost,
  [in, unique] NLM_SOCKADDR *destIPAddr
);

You pass in either NULL or a destination IP as destIPAddr, and get back a cost from pCost.

pCost is fine: cast it to an NLM_CONNECTION_COST, and you can see whether the NLM_CONNECTION_COST_UNRESTRICTED flag is set or not.

destIPAddr is slightly more tricky. We can either pass in NULL and receive the cost associated with the preferred connection used for machine internet connectivity, or pass in a specific IP and receive the cost associated with communicating with that IP. The snag is that the generated C# interop class has the method signature:

public void GetCost(out uint pCost, ref NLM_SOCKADDR pDestIPAddr);

This means that we can’t pass in NULL, so we’re going to have to pass in an IP. That’s fine, we’ll just look at the structure of NLM_SOCKADDR, and…

typedef struct _NLM_SOCKADDR {
  BYTE data;
} NLM_SOCKADDR, *PNLM_SOCKADDR;

Oh. A single byte. Not helpful. Let’s look at the generated C# struct:

public struct NLM_SOCKADDR
{
    public byte[] data;
}

That contains a byte array this time, but there’s no indication of what data it should contain.

If you go Googling enough, you might find this code snippet, which contains the snippets:

NLM_SOCKADDR destSocketAddress = {0};
hr = ConvertStringToSockAddr(destAddress, reinterpret_cast<SOCKADDR_STORAGE *>(&destSocketAddress));

Aha! That reinterpret_cast<SOCKADDR_STORAGE *>(&destSocketAddress) indicates that NLM_SOCKADDR is byte-for-byte compatible with the SOCKADDR_STORAGE structure, whose format is thankfully well documented. First off, the MSDN page for SOCKADDR_STORAGE says

With its padding, the SOCKADDR_STORAGE structure is 128 bytes in length.

This tells us that NLN_SOCKADDR.data should probably be set to a new byte[128], which is a good start. Follow the links, and you’ll find that SOCKADDR_STORAGE is itself byte-for-byte compatible with sockaddr. sockaddr can in turn be cast to a SOCKADDR_IN and a SOCKADDR_IN6, for IPv4 and IPv6 addresses respectively. Progress!

How do we get a SOCKADDR_IN or SOCKADDR_IN6 structure from an IP address? One way is to P/Invoke WSAStringToAddress, as demonstrated by pinvoke.net. This works (I tested it), but you then end up turning the sockaddr_in struct into the byte array needed by NLM_SOCKADDR.

Another way is to read the MSDN docs for SOCKADDR_IN and SOCKADDR_IN6, and write the right bytes to the right locations. This is the approach I took.

I didn’t find a way of calling GetCost without turning off “Embed Interop Types” in the properties for the “NETWORKLIST” reference.

using System.IO;
using System.Net;
using System.Net.Sockets;
using NETWORKLIST;

class NetworkListManager
{
    private const ushort AF_INET = 2;
    private const ushort AF_INET6 = 23;
    private const int sockaddrDataSize = 128;

    private readonly NetworkListManagerClass networkListManager;

    public NetworkListManager()
    {
        this.networkListManager = new NetworkListManagerClass();
    }

    public bool IsConnectionMetered(IPAddress address)
    {
        var sockAddr = (address.AddressFamily == AddressFamily.InterNetwork) ?
            CreateIpv4SockAddr(address) :
            CreateIPv6SockAddr(address);

        uint costVal;
        this.networkListManager.GetCost(out costVal, ref sockAddr);

        var cost = (NLM_CONNECTION_COST)costVal;

        return !cost.HasFlag(NLM_CONNECTION_COST.NLM_CONNECTION_COST_UNRESTRICTED);
    }

    private static NLM_SOCKADDR CreateIpv4SockAddr(IPAddress address)
    {
        var sockAddr = new NLM_SOCKADDR() { data = new byte[sockaddrDataSize] };

        // Seems to be compatible with SOCKADDR_STORAGE, which in turn is compatible with SOCKADDR_IN
        using (var writer = new BinaryWriter(new MemoryStream(sockAddr.data)))
        {
            // AF_INT
            writer.Write(AF_INET);
            // Port
            writer.Write((ushort)0);
            // Flow Info
            writer.Write((uint)0);
            // Address
            writer.Write(address.GetAddressBytes());
        }

        return sockAddr;
    }

    private static NLM_SOCKADDR CreateIPv6SockAddr(IPAddress address)
    {
        var sockAddr = new NLM_SOCKADDR() { data = new byte[sockaddrDataSize] };

        // Seems to be compatible with SOCKADDR_STORAGE, which in turn is compatible with SOCKADDR_IN6
        using (var writer = new BinaryWriter(new MemoryStream(sockAddr.data)))
        {
            // AF_INT6
            writer.Write(AF_INET6);
            // Port
            writer.Write((ushort)0);
            // Flow Info
            writer.Write((uint)0);
            // Address
            writer.Write(address.GetAddressBytes());
            // Scope ID
            writer.Write((ulong)address.ScopeId);
        }

        return sockAddr;
    }
}