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;
}
}