Exchange 2007 Ships with a tool called test-umconnectivity that can be used to verify the health of your Exchange Unified Messaging installation.
It has a couple of modes of operation:
Local Voice
In this mode, the task will run on a machine that has the Exchange Unified Messaging role installed. It will run diagnostics against the local UM server.
The way to run this, is to invoke test-umconnectivity without any parameters.
In this mode, the task verifies that the UMService is listening and responding to incoming INVITES.
[PS] D:\users\Administrator>test-umconnectivity
UmserverAcceptingCallAnsweringMessages : True
CallAnswerQueuedMessages : 0
CurrCalls : 0
UmIPAddress : 10.197.228.62
Latency : 2121
OutBoundSIPCallSuccess : True
EntireOperationSuccess : True
ReasonForFailure :
Identity : 62caff65-7a68-46ab-b2a3-ef52a31e431e
IsValid : True
Remote Voice
This is an end-to-end test. It verifies that the connectivity from UMServer to Gateway to PSTN is working fine. In this mode, the task does the following:
1) Makes an outbound call to a given phone number using a gateway.
2) The gateway will then send the call to the PBX.
3) The call will then come back to the gateway from the PBX.
4) The Gateway sends the call to a UM server that is linked to the gateway.
5) UM server will respond to the incoming INVITE with some DTMF tones, that are then verified by the task.
This test can be run from any machine that has the Exchange 2007 admin tools installed. It can also be run from any machine that has the Microsoft Operations Manager (MOM) installed, along with the Exchange Management Pack.
For example, I have a UM server setup with an IPGateway. I also have an autoattendant configured with an extension 74808. I want to make sure that my autoattendant is answering calls.
[PS] D:\users\Administrator>get-umipgateway
Name Address HuntGroups OutCallsAllowed Status
---- ------- ---------- --------------- ------
UNSECURE_GATEWAY 10.198.250... {74809:UNS... True Enabled
SECURE_GATEWAY securegw.e... {} True Enabled
sipphonegw 157.56.146.19 {:UNSECURE... True Enabled
[PS] D:\users\Administrator>get-umautoattendant
Name UMDialPlan PilotIdentifier SpeechEnabled Status
List
---- ---------- --------------- ------------- ------
aa1 UNSECURE_DIA... {74808} True Enabled
[PS] D:\users\Administrator>test-umconnectivity -UMIPGateway unsecure_gateway -Phone 74808
CurrCalls : 0
UmIPAddress : 10.197.228.62
Latency : 43236
OutBoundSIPCallSuccess : True
EntireOperationSuccess : True
ReasonForFailure :
Identity : 385bbfdf-26fc-460e-9ccb-a1f14b750c0b
IsValid : True
The output of the command shows which UM server answered the call, the call latency, etc.
TuiLogon
In this mode, the task will test that the UM server can connect to a mailbox in a site. This is used to make sure that the UM sever has good connectivity to mailbox servers that have the mailboxes for UM enabled user that the UM server is servicing.
In order to run this command, we need a UM enabled mailbox with an extension. Also, this command needs to be run on a UMServer.
[PS] D:\users\Administrator>test-umconnectivity -tuiLogon:1 -UMDialPlan:unsecure_dialplan -phone:74808 -pin:147258
While writing a unit test for a particular feature, I was faced with an interesting problem. My unit test has different scenarios. These scenarios test that a certain data item is propagated all the way down from the called function.
For eg, I have a method that I need to test - lets call it CFoo:DomSometing().
We pass in a data item to this method - so as part of the changes I change the method to CFoo::DoSomething(int data)
I need to make sure, in my tests, that the 'data' item is properly consumed by the function.
It turns out that for different functions that I call as part of each of my scenario, the way of checking is different. For eg, for one scenario, I have to call the function 'n' times, whereas for another scenario, I have to call the function for a particular duration.
I want to encapsulate all of this, so that I can write one test function that does the following
bool RunTest ( RunTestDelegate testMethod, int data, ILoopIterator iterator)
{
foreach (int i in iterator.GetValues())
{
// call the test method
testMethod (data);
}
}
Here, the RunTestDelegate will be used to encapsulate each method that I need to test, so that I dont have to write one RunTestXXX method per each method I want to test.
The question is: how do you implement the ILoopIterator? Clearly, the first requirement is that it should be returning an IEnumerable<int> because that is what the foreach loop is iterating over.
Also, as discussed above, we need it so that we can iterate over both a range of values, as well as for a time duration.
In order to solve this, I implemented the interface as follows:
interface ILoopIterator
{
IEnumerable<int> GetValues();
}
class LoopIterator : ILoopIterator
{
private int count;
private TimeSpan duration;
private bool isCounted;
private LoopIterator (TimeSpan duration)
{
this.duration = duration;
this.count = -1;
this.isCounted = false;
}
private LoopIterator (int iterationCount)
{
this.count = iterationCount;
this.isCounted = true;
this.duration = TimeSpan.MaxValue;
}
public static LoopIterator CreateTimed(TimeSpan duration)
{
return new LoopIterator (duration);
}
public static LoopIterator CreateCounted(int iterationCount)
{
return new LoopIterator (iterationCount);
}
public IEnumerable<int> GetValues()
{
if (this.isCounted)
{
for (int i = 0; i < this.count; i++)
{
yield return i;
}
}
else
{
int i = 0;
DateTime end = DateTime.Now + this.duration;
while (DateTime.Now < end)
{
yield return i;
++i;
}
}
}
}
This code used the .NET/2.0 C# features - i.e generics & yield statement for implementing enumerators.
The caller will use a different factory method, depending on how he wants the iteration to be performed.
For duration based iteration, use LoopIterator .CreateTimed(TimeSpan duration)
For normal iteration (like a for loop) use LoopIterator .CreateCounted(int count)
The utility of this pattern, is that the client (which is using ILoopIterator ) does not need to change!
This post is going to give an explanation of what the various greetings mean, for the autoattendant.
The posting will be divided into two sections. First, I will describe the common structure. Next, I will describe how this manifests itself at runtime for the DTMF AutoAttendant, and the ASR AutoAttendant respectively.
Greeting Configuration for the AutoAttendant
The greetings are configured for the AutoAttendant using the set-umautoattendant command in Powershell, or by using the Exchange Management Console MMC snapin that ships with Exchange Server 2007. When using set-umautoattendant, the following parameters are available to customize the greetings
| Prompt |
Description |
| InfoAnnouncementFilename |
Informational Announcement |
| InfoAnnouncementEnabled |
Informational Announcement Enabled Flag |
BusinessHoursWelcomeGreetingFilename AfterHoursWelcomeGreetingFilename |
Welcome Greeting Announcement |
BusinessHoursWelcomeGreetingEnabled AfterHoursWelcomeGreetingEnabled |
Welcome Greeting Announcement Enabled |
BusinessHoursMainMenuCustomPromptFilename AfterHoursMainMenuCustomPromptFilename |
Custom Menu Announcement |
BusinessHoursMainMenuCustomEnabled AfterHoursMainMenuCustomEnabled |
Custom Menu Enabled |
If you want to set the Autoattendant greetings using the Exchange Management Console MMC snapin, then you should navigate to the Greetings tab of the autoattendant properties page.
DTMF AutoAttendant Greeting ConfigurationMain Menu Greetings.
The Greetings for the Main Menu of the autoattendant are played in the following sequence.
First, the autoattendant plays the WelcomeGreeting defined on the AutoAttendant. If no greeting is defined, then the default greeting is played. The default welcome greeting is "Welcome to Microsoft Exchange Automated Attendant".
If the call was received during Holiday Hours (as defined in the holiday schedule), then the greeting associated with that holiday is played. After this, the Informational Announcement is played (if defined).
If a custom menu is defined, then the prompt defined in the AutoAttendant's BusinessHoursMainMenuCustomPromptFilename is played. The custom menu can be defined by setting the BusinessHoursCustomMenuKeyMapping. To enable the custom menu, set the BusinessHoursCustomMenuKeyMappingEnabled property to true.
If a custom menu prompt filename is not defined, then the server will do a best effort to render a TTS (Text to Speech) greeting using the CustomMenu options that were defined.
Custom Menu TTS
When a CustomMenu prompt filename is not defined, then the system will TTS the custom menu prompt. In order to TTS the prompt, it will sort the Custom menu in increasing order by the Key that has been defined. Then it will use greeting templates defined for the language of the autoattendant to generate Greeting segments, one for each menu item. The greeting segments are then concatenated to give the final greeting.
For a normal (non-timeout) menu option, the following greeting template is used to generate the greeting segment.
For <description> Press <key>
And if you have a timeout option defined in the custom menu, then the following template is used.
Or, stay on the line for <description>
The following table shows the greeting segment that are generated from the custom menu definition.
| Key |
Description |
Greeting Segment |
| 1 |
Sales |
For Sales Press One |
| 2 |
Support |
For SupportPress Two |
| Timeout |
Directions |
Or, stay on the line for Directions |
Once we have the greeting segment, the final greeting is generated by concatenating each individual greeting segment:
For Sales Press One. For Support, Press Two. Or, stay on the line for directions.
Autoattendant Greeting Scenarios
The following section describes how the autoattendant sounds when different combinaions of greetings are configured, along with some of the other options that effect greeting playback.
Default Configuration
This is the case where an autoattendant was created, and no settings were modified on the autoattendant. The autoattendant will sound as follows:
Welcome to Microsoft Exchange Automated Attendant.
Custom Welcome Greeting configured
You can change the welcomg greeting of the autoattendant by specifying a Business/AfterHours WelcomeGreetingFilename, and setting the corresponding Business/AfterHours WelcomeGreetingEnabled flag to TRUE. Once you do this, the system will sound as follows:
Welcome to Contoso Corporation.
Custom Informational Greeting configured
You can configure an informational greeting to let the callers know about extraordinary situations like unforeseen closures etc.. This greeting can be configured by setting the InfoAnnouncementFilename & InfoAnnouncementEnabled properties of the autoattendant. If you set this greeting, then the system will sound like this:
Welcome to contoso corporation. We are closed due to year end Inventory processing.
If you set the InfoAnnouncementEnabled property value to Uninteruptible, then this greeting cannot be interrupted by the caller. This means that the entire main menu greetings will become uninterruptible. This includes the welcome greeting, informational greeting, and the holiday greeting as well.
Holiday Greetings
You can also configure a holidy schedule for the autoattendant. When a call comes in during a holiday, the autoattendant will play a greeting specified for that holiday. Normally, it doesnt make sense to have both an Informational greeting and holiday greeting active at the same time, so we will assume for this scenario, that the informational greeting is not configured.
You can specify a holday schedule and greeting for the autoattendant by setting the autoattendant's HolidaySchedule property using Powershell. Or, if you are using the MMC, you can navigate to the Times tab, and set the Holiday Schedule there.
If you specify a holiday schedule for Christmas, for example, and also specify a greeting for that holiday, then this is how the main menu will sound when a user calls in.
Welcome to Contoso Corporation. The office will be closed on the 24th and 25th of December for Christmas. Normal hours will resume from the 26th of december.
Custom Menu Greeting
If a custom menu is defined, then the next greeting that will be played is the custom menu greeting. As explained earlier, the autoattendant will use the greeting defined in the Business/AfterHoursMainMenuCustomPromptFilename. If no greeting is defined, then a TTS greeting will be played to the caller.
If you assume that the custom menu has three options: Sales, Support, Directions as described in the section above, then this is what the caller will hear when no custom menu greeting is defined:
Welcome to Contoso Corporation. For Sales Press One. For Support, Press Two. Or, stay on the line for directions.
Additionally, if Transfer to operator and directory search are enabled for the autoattendant, the autoattendant will give the caller these options as well:
Welcome to Contoso Corporation. For Sales Press One. For Support, Press Two. Or, stay on the line for directions. To contact someone, press the Pound key. To speak to an operator, press Zero.
If the call came in during a holiday, then the holiday greeting will be played after the welcome greeting. In that case, this is what the callers will hear:
Welcome to Contoso Corporation. The office will be closed on the 24th and 25th of December for christmas. Normal hours will resume from the 26th. of December.
For Sales Press One. For Support, Press Two. Or, stay on the line for directions. To contact someone, press the Pound key. To speak to an operator, press Zero.
However, if you define a greeting for the custom menu, then this is what the caller will hear:
Welcome to Contoso Corporation. The office will be closed on the 24th and 25th of December for christmas. Normal hours will resume from the 26th. of December.
To talk to our sales department, press One. For support issues, press Two. Or, you can stay on the line for directions.
If you compare this greeting flow with the flowchard given in the begining, you will noticed that the custom menu greeting did not give the caller an option for transferring to the operator, or doing directory search by pressing the Pound key. If you want to give callers this option, you must record these into your custom menu greeting.
If you dont want to give callers this option, you can disable them on the autoattendant.
If you are the administrator of an Exchange Unified Messaging server, you might want to know why the UM worker process is recycling and how often. The following one line PowerShell script will print out all the reasons why the process recycled:
get-eventlog application | where { ($_.EventId -ge 1049 -and $_.EventId -le 1055) -or $_.EventId -eq 1092 } | ft -wrap
If you run this command, you will get an output like so:
Index Time Type Source EventID Message
----- ---- ---- ------ ------- -------
56821 Mar 12 20:35 Info MSExchange Unified M 1055 The Unified Messaging Worker Process was terminated because the confi
9 essaging gured maximum lifetime was exceeded.
56773 Mar 12 20:05 Info MSExchange Unified M 1000 The Unified Messaging Worker Process was started successfully on port
4 essaging "5065".
56772 Mar 12 20:04 Info MSExchange Unified M 1051 The Microsoft Exchange Unified Messaging service has created a new UM
5 essaging WorkerProcess because the working set (589 MB) has exceeded the conf
igured maximum: 500 MB"
52146 Mar 11 03:55 Info MSExchange Unified M 1055 The Unified Messaging Worker Process was terminated because the confi
1 essaging gured maximum lifetime was exceeded.
52099 Mar 11 03:25 Info MSExchange Unified M 1000 The Unified Messaging Worker Process was started successfully on port
2 essaging "5067".
52098 Mar 11 03:24 Info MSExchange Unified M 1051 The Microsoft Exchange Unified Messaging service has created a new UM
1 essaging WorkerProcess because the working set (777 MB) has exceeded the conf
igured maximum: 500 MB"
49992 Mar 10 02:55 Info MSExchange Unified M 1055 The Unified Messaging Worker Process was terminated because the confi
1 essaging gured maximum lifetime was exceeded.
49948 Mar 10 02:25 Info MSExchange Unified M 1000 The Unified Messaging Worker Process was started successfully on port
6 essaging "5065".
49947 Mar 10 02:24 Info MSExchange Unified M 1051 The Microsoft Exchange Unified Messaging service has created a new UM
7 essaging WorkerProcess because the working set (644 MB) has exceeded the conf
igured maximum: 500 MB"
Many a time, I had to look at eventlog files sent by a customer, or those from a test run that happened some time back. These files (.evt) are offline copies of eventlogs. To view them, you open them up with eventviewer.
However, you might sometimes notice that the display of these logs does not give the descriptive information that was logged. It will only show the parameters used when logging the message. For eg, if you create an eventlog entry with the followin format string:
"This operation failed with error %s"
then, the display will only show the parameter that was passed to the format string.
To work around this, you can use the /AUXSOURCE= flag to point eventviewer at another machine that might have the required resources to load the resource strings. This is done by invoking the eventviewer in the following manner:
mmc.exe /a eventvwr.msc /AUXSOURCE=<machine name or ip>
I have been stumped from time to time on how to attach VS debuggers to a process on process startup. I knew how to do it with Windbg, by setting ImageFileExecutionOptions for the target process. However I did not know how to do it for VS.
Well, I need to fret no more. A colleague forwarded me th is blog entry by Greg, in which he has some good info on how to use ImageFileExecutionOptions to attach to a process on startup.
http://blogs.msdn.com/greggm/archive/2005/02/21/377663.aspx
In this part, we will add some networking code to the code we have thus far. When we get done, we should have a working Ping utility.
Take the program that we wrote in the Ping: Part III and add the following code.
using System;
using System.Text;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
namespace ping
{
public class ICMP_PACKET
{
public ICMP_PACKET(Byte kind, Byte code, UInt16 id, UInt16 seq, byte[] data)
{
...
}
public ICMP_PACKET(byte[] data, int offset, int count)
{
this.i_type = data[offset++];
this.i_code = data[offset++];
this.i_cksum = ByteToUint(data, offset);
offset += 2;
this.i_id = ByteToUint(data, offset);
offset += 2;
this.i_seq = ByteToUint(data, offset);
offset += 2;
this.data = new byte[count - headerLength];
Array.Copy(data, offset, this.data, 0, this.data.Length);
}
public static UInt16 ByteToUint(byte [] networkBytes, int offset)
{
return BitConverter.ToUInt16(new byte[] { networkBytes[offset+1], networkBytes[offset] }, 0);
}
}
class Program
{
static void Main(string[] args)
{
if (args.Length != 1)
{
Usage();
}
if (0 == String.Compare(args[0], "/?", true, CultureInfo.InvariantCulture)
|| 0 == String.Compare(args[0], "-h", true, CultureInfo.InvariantCulture)
|| 0 == String.Compare(args[0], "-?", true, CultureInfo.InvariantCulture))
{
Usage();
}
string target = args[0];
IPAddress [] heTarget = Dns.GetHostAddresses(target);
IPAddress ipTarget = null;
foreach (IPAddress ip in heTarget)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
ipTarget = ip;
break;
}
}
IPAddress[] heSource = Dns.GetHostAddresses(Dns.GetHostName());
IPAddress ipSource = null;
foreach (IPAddress ip in heSource)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
ipSource = ip;
break;
}
}
IPEndPoint epLocal = new IPEndPoint(ipSource, 0);
Socket pingSocket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);
pingSocket.Bind(epLocal);
byte [] data = Encoding.ASCII.GetBytes("1234567890abcdef");
Console.WriteLine("Ping {0}({1}) with {2} bytes of data...",target, ipTarget.ToString(), data.Length);
Console.WriteLine();
for (int i = 0; i < 3; i++)
{
ICMP_PACKET packet = ICMP_PACKET.CreateRequestPacket(111, 222, data);
IPEndPoint epRemote = new IPEndPoint(ipTarget, 0);
pingSocket.SendTo(packet.Serialize(), epRemote);
byte[] receiveData = new byte[1024];
EndPoint epResponse = (EndPoint)new IPEndPoint(0, 0);
int read = pingSocket.ReceiveFrom(receiveData, ref epResponse);
ICMP_PACKET recvPacket = new ICMP_PACKET(receiveData, 20, read);
Console.WriteLine("Reply from:{0} bytes = {1}", ((IPEndPoint)epResponse).Address.ToString(), recvPacket.PayloadLength);
}
}
...
}
A couple of important points that I wanted to bring to your attention:
1) I have uncommented the code which checks the validity of command-line parameters in Program::Main.
2) We have added a new constructor ICMP_PACKET(byte [], int, int). This is to parse the response returned by the server.
3) The ByteToUInt(byte [], int) method is used to convert from Network Order to Host Order. This is needed because the bytes on the wire are in Network Order, which is BigEndian
4) In Main(), when we resolve the IPAddress of the source and destination, we need to make sure that we work with the correct IP address. Since this program is limited to IPv4, we need to make sure that we do not accidently work with IPv6 addresses. The IPv6 address may be returned when the host that you are trying to resolve is IPv6 enabled.
5) When we read the response from the socket, we get back more bytes that the size of the ICMP packet sent by the server. This is because we opened the Socket with SocketType.Raw. In this mode, we will also get back the IPv4 header that is encapsulating the ICMP packet. So, when we process the response packet, we need to make sure that we skip past the IP header. That is why we skip the first 20 bytes of the packet when we try to create an ICMP_PACKET with the returned data.
If you compile and run this program, you will be able to ping a host without any problems.
As utilities go, this is a very barebones utility. It does not handle errors from the network very well. For the interested, here are some things that you can do to this to make it more robust:
-
Be ready to handle exceptions from Socket.SendTo() and Socket.ReceiveFrom().
-
The utility does not handle timeouts. It would be a simple matter to add code and set a timeout on the Receive() call. That way, if the response doesnt come back within a certain time interval, you can print an error ( or an asterisk like the Ping utility that comes with the OS).
-
Give a command line option to change the #bytes sent in the payload.
Well, that is it then. Now we have a working Ping utility. Hopefully you have learned how to convert a protocol specification into a working program.
In Part II of this article, we saw how we are going to use the ICMP protocol to implement a simple Ping client. We also saw a skeleton of this program which showed how to translate the ICMP packet specification into a C# structure.
In this part, we will write a routine to calculate the checksum of the packet, and a routine to serialize the packet into a byte array. Recall from PartII that the request and reply packets have a particular encoding on the wire. We will have to write a routine that will encode the packet into a byte array, so that the array can be sent on the wire.
Take the skeleton that we created in Part II and add the following (lines in blue):
using System;
using System.Text;
using System.Globalization;
namespace ping
{
public class ICMP_PACKET
{
...
public ICMP_PACKET(Byte kind, Byte code, UInt16 id, UInt16 seq, byte[] data)
{
this.i_type = kind;
this.i_code = code;
this.i_id = ToNetworkOrder(id);
this.i_seq = ToNetworkOrder(seq);
this.data = data;
this.i_cksum = Checksum();
}
public static ICMP_PACKET CreateRequestPacket(UInt16 id, UInt16 seq, byte[] data)
{
return new ICMP_PACKET(8, 0, id, seq, data);
}
public static byte[] GetBytesInNetworkOrder(UInt16 number)
{
byte[] bytes = BitConverter.GetBytes(number);
if (BitConverter.IsLittleEndian)
return new byte[] { bytes[1], bytes[0] };
else
return bytes;
}
public static UInt16 ToNetworkOrder(UInt16 number)
{
byte[] networkBytes = GetBytesInNetworkOrder(number);
return BitConverter.ToUInt16(networkBytes, 0);
}
public UInt16 Checksum()
{
int cksum_buffer_length = (int)(Length / 2);
byte [] packetBytes = Serialize(this);
//
// first, convert the serialized bytes into a UInt16 array
// we will use the UInt16 array to do the checksum
//
UInt16[] checksumBuffer = new UInt16[cksum_buffer_length];
int index = 0;
int i = 0;
while (index < Length)
{
checksumBuffer[i] = BitConverter.ToUInt16(packetBytes,index);
index += 2;
++i;
}
int checksum = 0;
for (i = 0; i < checksumBuffer.Length; i++)
{
checksum += Convert.ToInt32(checksumBuffer[i]);
}
checksum = (checksum >> 16) + (checksum & 0xffff);
checksum += (checksum >> 16);
return (UInt16)(~checksum);
}
public byte[] Serialize()
{
return Serialize(this);
}
public static byte[] Serialize(ICMP_PACKET packet)
{
// first, find out how many bytes to allocate for the serialized packet
int packet_size = packet.Length;
bool isLittleEndian = BitConverter.IsLittleEndian;
UInt16 cksum = packet.i_cksum;
UInt16 id = packet.i_id;
UInt16 seq = packet.i_seq;
//allocate the byte array
byte[] packetArray = new byte[packet_size];
// now serialize the packet into the array.
int index = 0;
packetArray[index++] = packet.i_type;
packetArray[index++] = packet.i_code;
// the checksum is 16 bits.
byte[] temp = BitConverter.GetBytes(cksum);
// copy it into the packetArray at the required offset
Array.Copy(temp, 0, packetArray, index, temp.Length);
index += 2;
// similarly, copy the rest.
temp = BitConverter.GetBytes(id);
Array.Copy(temp, 0, packetArray, index, temp.Length);
index += 2;
// copy seq#
temp = BitConverter.GetBytes(seq);
Array.Copy(temp, 0, packetArray, index, temp.Length);
index += 2;
// copy payload
if (packet.PayloadLength > 0)
{
Array.Copy(packet.data, 0, packetArray, index, packet.PayloadLength);
}
return packetArray;
}
};
class Program
{
static void Main(string[] args)
{
//if (args.Length != 1)
//{
// Usage();
//}
//if (0 == String.Compare(args[0], "/?", true, CultureInfo.InvariantCulture)
//|| 0 == String.Compare(args[0], "-h", true, CultureInfo.InvariantCulture)
//|| 0 == String.Compare(args[0], "-?", true, CultureInfo.InvariantCulture))
//{
// Usage();
//}
byte [] data = new byte[32];
int j = 0;
for (int i = 0; i < 32; i++)
{
data[i] = (byte)((int)'a' + j);
j = (j + 1) % 23;
}
ICMP_PACKET packet = ICMP_PACKET.CreateRequestPacket(0x200, 0x9603, data);
byte[] serialized = packet.Serialize();
for (int i = 0; i < serialized.Length; i++)
{
Console.Write("{0:X} ", serialized[i]);
}
}
...
}
}
A couple of important points that I wanted to bring to your attention:
1) I have commented out the code which checks the validity of command-line parameters. While we are developing this application, we will sometimes be running this program with no command line arguments, so I didn’t want that code there. We will put it back in later.
2) As you might recall, the Platform/CPU stores data in a certain order. This is called Endianness. Intel (X86) platform, on which I am writing this program is Little-Endian. However it turns out that the byte-order on the network is always Big-Endian. Hence we need to convert multi-byte numbers from LittleEndian to Big-Endian before we send them on the network. Therefore, I have written the functions GetBytesInNetworkOrder() and ToNetworkOrder() to do the conversion from Little-Endian to Big-Endian.
3) The functions Serialize() and Checksum() are used to serialize the packet into a byte array, and calculate the checksum respectively. Note that as per the RFC, while calculating the checksum, we should assume that the i_cksum field of the packet structure has a value of zero. Once the checksum is calculated, we write the value into the structure, and then serialize the structure to get the final bytes on the wire.
4) In Part II of this series, I showed you the network trace of the Ping.exe routine that comes with the OS. In this part, I created a request packet with the same data, to make sure that our serialization routine and checksum routines are right. You can compile the program, and run it to verify.
D:\ping>bin\debug\ping.exe
8 0 B5 58 2 0 96 3 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 7
5 76 77 61 62 63 64 65 66 67 68 69
If you compare this output with the Ethereal trace of the actual Ping command that I ran in Part II, you will see that they match. This tells us that the Checksum and serialization routines that we wrote are correct.
If you see the ping utility that comes with your OS, you will notice that it has many options. However, the one we are going to develop will just take one argument:
C:\ping>ping <hostname> | <ipaddress>
Example: ping www.contoso.com
Example: ping 127.0.0.1
Let us start out by looking at requirements of the Ping client. Basically, the task of the tool is to find out if a specified server is alive, and on the network. The way it achieves this is by sending an “echo” packet to the server. The server responds with an “echo” response. If the server responds within a certain time interval, then we can assume that there is network connectivity from the client to the server. If it doesn’t respond, then it could indicate a variety of things that could be wrong, for eg:
- The destination server is not up.
- The destination server is up but does not have networking enabled.
- The destination server is up and on the network, but is configured to drop Ping requests
- Etc.
We will not concern ourselves with “why” a server is not responding to the Ping request, as it could be another topic in itself.
Where was I? Oh yeah, the Ping client. It so happens that we have a protocol (ICMP) that has the echo request and reply operations that we need to implement the Ping client. Read the ICMP RFC to get information about all the features offered by the ICMP protocol. As we know, the ICMP protocol is at the same layer as the IP protocol in the OSI layering scheme.
For the ping utility, we will be using the ICMP Echo Request and response messages. The client will send the ICMP echo request message, and the server will reply with an Echo response message. If you look at the Rfc, it describes how an RFC message looks like:
|
Offset |
Field Name |
Size (octets) |
Description |
|
0 |
Type |
1 |
Specified type of the operation. It should be set to 8 for request message and 0 for reply messages |
|
8 |
Code |
1 |
This field is Zero. |
|
16 |
Checksum |
2 |
One’s complement checksum of the ICMP message |
|
32 |
Identifier |
2 |
Used to correlate request and reply |
|
48 |
Sequence Number |
2 |
Used to correlate request and reply |
|
64 |
Data |
Variable |
Optional data to be sent with the request. This must be sent back by the server. |
Looking at the table above, we can come up with a C# class that corresponds to what the packet will look like:
public class ICMP_PACKET
{
public Byte i_type; // type of message
public Byte i_code; // sub code
public UInt16 i_cksum; // ones complement checksum of header
public UInt16 i_id; // identifier
public UInt16 i_seq; // sequence number
public Byte[] data;
}
I used Ethereal to sniff the network while doing a Ping, to see what a Ping request/response looks like on the wire. The request packet looks like this:
0020 59 99 08 00 b5 58 02 00 96 03 61 62 63 64 65 66 Y....X.. ..abcdef
0030 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 ghijklmn opqrstuv
0040 77 61 62 63 64 65 66 67 68 69 wabcdefghi
Let us dissect the packet:
i_type = 0x08 ( Echo Request)
i_code = 0x00
i_cksum = 0xb558
i_id = 0x0200
i_seq = 0x9603
data = 32 bytes ( abcedf…. )
The ICMP echo reply looks as follows:
0020 59 9a 00 00 bd 58 02 00 96 03 61 62 63 64 65 66 Y....X.. ..abcdef
0030 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 ghijklmn opqrstuv
0040 77 61 62 63 64 65 66 67 68 69 wabcdefghi
Let us dissect the packet:
i_type = 0x00 ( Echo Reply)
i_code = 0x00
i_cksum = 0xbd58
i_id = 0x0200
i_seq = 0x9603
data = 32 bytes ( abcedf…. )
Given what we have learned so far, we can now come up with a skeleton for the program that we will develop:
using System;
using System.Text;
using System.Globalization;
namespace ping
{
public class ICMP_PACKET
{
Byte i_type; // type of message
Byte i_code; // sub code
UInt16 i_cksum; // ones complement checksum of header
UInt16 i_id; // identifier
UInt16 i_seq; // sequence number
Byte[] data;
private const int headerLength = 8;
// Total size of the packet (header + payload)
public int Length
{
get { return HeaderLength + PayloadLength; }
}
// Size of the packet header (excluding data)
public int HeaderLength
{
get { return headerLength; }
}
// Size of the payload
public int PayloadLength
{
get { return (data == null) ? 0 : data.Length; }
}
public byte PacketType
{
get { return i_type; }
}
public byte Code
{
get { return i_code; }
}
public UInt16 Identifier
{
get { return i_id; }
}
public UInt16 SequenceNumber
{
get { return i_seq; }
}
public ICMP_PACKET(Byte kind, Byte code, UInt16 id, UInt16 seq, byte[] data)
{
this.i_type = kind;
this.i_code = code;
this.i_id = id;
this.i_seq = seq;
this.data = data;
this.i_cksum = 0;
}
public static ICMP_PACKET CreateRequestPacket(UInt16 id, UInt16 seq, byte[] data)
{
return new ICMP_PACKET(8, 0, id, seq, data);
}
};
class Program
{
static void Main(string[] args)
{
if (args.Length != 1)
{
Usage();
}
if (0 == String.Compare(args[0], "/?", true, CultureInfo.InvariantCulture)
|| 0 == String.Compare(args[0], "-h", true, CultureInfo.InvariantCulture)
|| 0 == String.Compare(args[0], "-?", true, CultureInfo.InvariantCulture))
{
Usage();
}
}
static void Usage()
{
Console.WriteLine("ping <hostname> | <ipaddress>");
Console.WriteLine("\tExample: ping www.contoso.com");
Console.WriteLine("\tExample: ping 127.0.0.1");
Environment.Exit(0);
}
}
}
After a long hiatus, I am back to posing to my blog. I am going to start out with a series on Socket programming.
In this series, I will show you how to implement a simple Ping client using classes from the System.Net.Sockets namespace. It should be an exciting journey.
For starters, you can familiarize yourself with the RFC for ICMP (Internet Control Message Protocol) which is the protocol used by the PING client. The RFC for that is here: (http://www.ietf.org/rfc/rfc0792.txt).
Until next time,
Cheers.
In a previous blog post, I wrote about how printer manufacturers are embedding tracking information in their printers.
Well, today, via Slashdot, I learned that folks at EFF have figured out the encoding in atleast one printer (those made by Xerox).
The following link has more details... http://www.eff.org/news/archives/2005_10.php#004063
Durgaprasad, the test lead for System.Net has a very informative entry on using Tracing facilities in whidbey.
http://blogs.msdn.com/dgorti/archive/2005/09/18/471003.aspx
In whidbey, System.Net has a cool retail tracing implementation. It writes most calls made to public API's to a TraceListener. You can setup your own tracelisteners, or use the config file to setup a default listener.
This facility is very useful in debugging problems with applications.
Here is the example config file which enables Verbose tracing.
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="System.Net">
<listeners>
<add name="System.Net"/>
</listeners>
</source>
<source name="System.Net.Sockets">
<listeners>
<add name="System.Net"/>
</listeners>
</source>
<source name="System.Net.Cache">
<listeners>
<add name="System.Net"/>
</listeners>
</source>
</sources>
<sharedListeners>
<add
name="System.Net"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="System.Net.trace.log"
/>
</sharedListeners>
<switches>
<add name="System.Net" value="Verbose" />
<add name="System.Net.Sockets" value="Verbose" />
<add name="System.Net.Cache" value="Verbose" />
</switches>
</system.diagnostics>
</configuration>
NOTE: If you are doing logging inside of the ASP.NET process, make sure to give the ASP.NET process identity WRITE permissions to the directory where you want the log to be written.
A user posted on the newsgroups asking about how to set Host headers in HttpWebRequest.
HttpWebRequest does not allow the host header to be set by the user. It is set automatically from the URI supplied to HttpWebRequest constructor.
However, it turns out that there is a roundabout way to accomplish this. This is shown in the following reply to the original post:
http://groups-beta.google.com/group/microsoft.public.dotnet.framework/browse_thread/thread/17c5369520221c8/4e768c733ad2f451?q=HttpWebRequest+and+Host+Header&rnum=1#4e768c733ad2f451
This solution works, but there is a subtle difference in the request sent on the wire. To illustrate that, look at the following program:
using System;
using System.Net;
public class EP {
public static void Main()
{
test("http://www.company.com/test.aspx");
test("http://www.contoso.com/test.aspx");
}
public static void test(string uri)
{
WebRequest req = WebRequest.Create(uri);
req.Proxy = new WebProxy("http://myserver", false);
WebResponse resp = req.GetResponse();
resp.Close();
}
}
To get this program to work, you need to do the following:
- Have a server named "myserver" on which you get IIS/ASP.NET running. If you dont want to run ASP.NET that is fine as well.
- On the server, go into the IIS console, and create two websites. Set the host header on the first website to be www.company.com and the other to be www.contoso.com
- Compile and run the above program.
If you look at the request sent on the wire, you will notice that this program sends the following request to the server "myserver"
GET http://www.contoso.com/test.aspx HTTP/1.1
Host: http://www.contoso.com
Whereas the actual request you want sent is:
GET /test.aspx HTTP/1.1
Host: www.contoso.com
NOTE: Even though the request URI sent on the wire is an absolute URI, and is different from what should actually be sent if the client had control of the host header, it will still work on the server, because as per RFC/2616, a server is supposed to honor all requests which use an absolute URI instead of a relative URI to identify the resource.
When doing authentication, HttpWebRequest manages connections differently, depending on the authentication mode being used. If it uses Windows Integrated Authentication, it will close the connection after every request completes. The reasons for this are complex, and I wont go into them at this point.
However, there are cases where you dont want this to happen. For example, imagine that you have a 3-tier architecture, where the client is talking to an asp.net application, which in turn is hitting a backend webserver. The asp.net webapplication (henceforth called the MiddleTier) is using HttpWebRequest to talk to the back-end, and the back-end is secured by Windows Integrated Authentication.
If the middle-tier web application ends up getting a lot of requests, it will inturn issue a lot of requests to the back-end. The default behavior of webrequest is to create a new connection for each request, and over time, the middle-tier might end up running out of wildcard TCP ports, causing the HttpWebRequest to fail with a "Unable to connect to the remote server" exception.
To mitigate this, you want to use a property on the webrequest called UnsafeAuthenticatedConnectionSharing . Setting this property will cause HttpWebRequest to reuse authenticated connections (making sure that it honors ServicePoint.ConnectionLimit).
Security Note: You dont want to use this property lightly. It has security consequences.