Peer To Peer in Windows Vista - Part 2 - PNRP

Today we are going to look at a few different methods. The first set of methods allows us to register a PNRP name and the next set of methods allows us to resolve a PNRP name. A few important things to keep in mind are that the Windows Firewall Client will block traffic so this code will not work between machines unless we add and exception to the Windows Firewall client. For now you can manually add the exception or work between two instances running on the same machine. PNRP will not resolve a name registered in the same process, so you need to run two separate instances to make PNRP work. We will come back to the Windows Firewall problem in a feature post when we look at those APIs.

In my last post I talked about the libs and header files required to compile so reference that post if you are having problems.

 

Most of the peer to peer APIs are exposed as native Win32 libraries. This means we either have to use PINVOKE or write a C++\CLI 2005 wrapper. I have chosen to write a wrapper, because the peer to peer APIs require special methods to release the memory from the structures they create. If you are new to writing interop code you should get a copy of “.Net and Com The Complete Interoperability Guide” by Adam Nathan.

 

For a deep understanding of how PNRP works under the covers check out this article. Here is the code for the register method. We begin by looking a method that allows us to register a PNRP name. PNRP is a “DNS-like” protocol accept that it is server-less. Something to keep in mind is I have removed all error checking from this sample to make it easier to understand.

 

HRESULT P2PCom::Peer::PnrpRegister(PWSTR pwzName)

{

      ULONG cAddresses = 0;

      HRESULT hr = S_OK;

     

      // Address locals

      ULONG pcAddresses = PEER_PNRP_AUTO_ADDRESSES;

      PWSTR pwzPeerName = NULL;

      PEER_PNRP_REGISTRATION_INFO info;

      HANDLE registration;

     

ZeroMemory(&info, sizeof(PEER_PNRP_REGISTRATION_INFO));

     

      // Create the peer name.

      hr = PeerCreatePeerName(NULL, pwzName, &pwzPeerName);

     

// We need the cloudName and address to register the PNRP name.

      info.pwzCloudName = L"Global_";

      info.cAddresses = pcAddresses;

           

      // Register the PNRP name in the cloud.

      hr = PeerPnrpRegister(pwzPeerName, &info, &registration);

     

// Clean up

      PeerFreeData(pwzPeerName);

   

      return hr;

}

Breaking down the PnrpRegister method we first call the PeerCreatePeerName function which creates a PNRP name from the string that is passed into the method. We passed NULL to the first method because we want to create an unsecured PNRP name. If we wanted to have a secured name then we would have to pass the results from a call to PeerIdentityCreate fuction. The article I pointed to earlier will explain the difference between secured and unsecured PNRP names in detail, but basically an unsecure name is not unique a secured name is unique. We passed Ernie into this method we would get 0.Ernie as a result of the call to PeerCreatePeerName. The zero dot in front of Ernie means this is an unsecured peername a secured PNRP name would have a long hash of a public key infront of it.

Next we assign the name of the cloud we want to register the PNRP name in to a PEER_PNRP_REGISTRATION_INFO structure called info. In this case we are registering in the global cloud which means the internet. There are other options for clouds shown in the table below from the Windows SDK:

Value

Meaning

PNRP_SCOPE_ANY0

All IP addresses are allowed to register with the PNRP cloud.

PNRP_GLOBAL_SCOPE1

The scope is global; all valid IP addresses are allowed to register with the PNRP cloud.

PNRP_SITE_LOCAL_SCOPE2

The scope is site-local; only IP addresses defined for the site are allowed to register with the PNRP cloud.

PNRP_LINK_LOCAL_SCOPE3

The scope is link-local; only IP addresses defined for the local area network are allowed to register with the PNRP cloud.

The other field of the structure we specifiy is cAddresses which we set to PEER_PNRP_AUTO_ADDRESSES. This tells PeerPnrpRegister how to bind our PNRP name to our IP address. Last we call PeerPnrpRegister which actually does the PNRP name registration.

The method below is the .NET wrapper method to expose the native method we just looked at. It simply marshals the name that is passed in and calls the native PnrpRegister method.

void P2PCom::Peer::RegisterName(System::String ^name)

{

      System::IntPtr ^piName = Marshal::StringToBSTR(name);

      PWSTR pName = (PWSTR)piName->ToPointer();

     

      PnrpRegister(pName);

      Marshal::FreeBSTR(*piName);

}

Now that we have registered a name we need to resolve the name. IMPORTANT: you cannot resolve a PNRP name register in the same process you are doing the resolve in, this is a big gotchya when first developing with this API. Take a look at the source code listing then we discuss what is happening here.

IPAddress ^P2PCom::Peer::ResolvePeerNameCommand(PCWSTR pwzName)

{

      HRESULT hr = S_OK;

   PPEER_PNRP_ENDPOINT_INFO pEndpointInfo = NULL;

    ULONG cEndpoints = MAX_ENDPOINTS_TO_RESOLVE;

      // Perform a synchronous resolve

      hr = PeerPnrpResolve(pwzName, L"Global_", &cEndpoints, &pEndpointInfo);

     

if(pEndpointInfo == NULL)

      {

            return IPAddress::None;

      }

     

      WCHAR wzAddr[MAX_PEERNAME_LENGTH];

 

    // Display associated addresses

    for (int i = 0; i < pEndpointInfo->cAddresses; i++)

    {

        DWORD dwLen = (sizeof(wzAddr) / sizeof(wzAddr[0]));

           

            // Make sure the address is IPv6

            if(pEndpointInfo->ppAddresses[i]->sa_family == AF_INET6)

            {

                  hr = WSAAddressToString(

                (LPSOCKADDR) pEndpointInfo->ppAddresses[i],

                sizeof(SOCKADDR_IN6), NULL, wzAddr, &dwLen);

            }

      }

// Release resources.

PeerFreeData(pEndpointInfo);

      IPAddress ^ip = IPAddress::Parse(gcnew System::String(wzAddr));

    return ip;

}

We only make one P2P PNRP API call in this method to PeerPnrpResolve which attempts to do a sycronous resolve of the method. There are also async versions of this method which make sense to use in GUI applications as this method can takes a few seconds to return. It will return all the pnrp end points it found in the pEndpointInfo argument. That is end points plural because I could register the same end point at work, home and on my laptop, also with unsecure names there will likely be other people with the same end point name as mine.

Next check to see if we received any end points and then loop through all the end points we received. We only care about those addresses that are IPv6 for this sample. Change the last endpoint into a string and save it in the wzAddr variable using a WinSock helper method. Then we create a managed IPAddress by calling it’s Parse method on the address string.

Last thing is to write a wrapper method so that .NET applications can call this method.

System::Net::IPAddress ^P2PCom::Peer::ResolveName(System::String ^name)

{

      System::IntPtr ^piName = Marshal::StringToBSTR(name);

      PWSTR pName = (PWSTR)piName->ToPointer();

     

      IPAddress ^ip = ResolvePeerNameCommand((PWSTR)pName);

      Marshal::FreeBSTR(*piName);

     

      return ip;

}

In my full class I move all the start up an shutdown code to my constructor and destructor as shown here:

P2PCom::Peer::Peer()

{

      PeerPnrpStartup(PNRP_VERSION);

}

P2PCom::Peer::~Peer()

{

      PeerPnrpShutdown();

}

There is a lot more to PNRP and there is a great sample in the SDK located here:

“C:\Program Files\Microsoft SDKs\Windows\v6.0\Samples\NetDs\PeerToPeer\PNRP”