Click here to Skip to main content
Click here to Skip to main content
Go to top

Server Traffic Redirector

, 21 Oct 2005
Rate this:
Please Sign up or sign in to vote.
An article on how to redirect external network traffic to an internal network.

Comamnd Prompt with Redirector in action

Introduction

This article is about overcoming firewall-ing issues with regards to .NET Remoting. The basic problem with .NET Remoting is that if you have a Remoting server located on a private network which is protected by a firewall (it does not matter at this point what kind of firewall it is!), clients are bound to be hooked up to the Remoting server outside the firewall. Internal clients are not a problem here.

Remoting has that big weakness surrounding firewalls, especially if Microsoft's own native Remoting layers are used, which I feel is the reason I was compelled to hack up this code. The one big caveat that I must say, is it will definitely work for bi-directional traffic communications, in other words, in a nutshell, a bi-directional socket which is TCP! This has served its purpose with regards to Genuine Channels which is what I use for the Remoting layer (I didn't want my remoting server to be sitting on the same machine as the firewall itself!! - Am merely practicing safe hex! :-P). I do not know if this will work with Microsoft's own native Remoting layers, i.e., TCP channel. It's disappointing to see that in the upcoming Whidbey/VS.NET 2005, Microsoft's native TCP channel is not even bi-directional!

But, the nice thing about this code, is much more than that - it can be used for any other bi-directional network communications, command line is driven with very friendly command line switches, it uses asynchronous sockets, it can provide you with statistics which are approximate, and dumps the incoming network data to where you want it to go (this code's motto should be 'Where do you want the dump to go today?'). That's a joke, if you don't get it, here's a hint - Microsoft's motto and general messages.

Background

I was reading a very recent article in MSDN (August 2005), the topic was about getting high performance out of sockets, and it was this, that I used as a foundation for the code; and also looking at Ingo Rammer's Open Sourced Bi-Directional TCP Channel, specifically how TCP channels were written, that idea of using ConnectionManager came about as well.

The pre-requisite to using the code is that the Genghis library must be available as I have used their excellent CommandLineParser class. Look it up via Google (Genghis + www.sellsbrothers.com keywords, I cannot remember the exact URL as the URL I've supplied at the bottom - I haven't used that URL!).

In the Zip file attached, with the debug build, you need to download the latest and greatest copy of Genghis and add it as a reference to this project and away you go. The Genghis download is about approx 3.5 MB so I cannot and should not include it as I'm respecting the Genghis crowd regarding licensing. Even though it's free, I feel you should go and download it and give them crowd an applause! Smile | :)

Yes, I do use Genuine Channels for my Remoting needs and it does work, but hey, you should check out Ingo's latest book - Advanced .NET Remoting, it is a fine read. I'd re-read it again (have read it so many times and say 'Wow - how the .... did you find out all that information' and left with a feeling that it was a worthy investment into reading it....) Also, I most certainly give Dmitry Belikov, the guru behind Genuine Channels, the thumbs up and also a worthy investment - enough of my three Rs - ranting/rambling/raving!

Using the code

Overview of the Program Executable

When you build the attached solution, invoke it on the command line like this:

SrvrRedirector /?

The output would be:

Socket Redirector v1.1.2101.29722
(C) Copyrighted by Tom Brennan, 2005. All Rights Reserved. Unauthorised use & di
stribution is prohibited.
This hopefully, will capture the incoming network data from a source that is on
external network, that arrives at this redirector and silently connects to the d
estination which would be on internal network, thus overcoming Firewall issues.

Usage: ReDirSrvr.exe [@argfile] /srcIP:<value> /srcPort:<value> /dstIP:<value>
       /dstPort:<value> [/maxConn:<10>] [/bufSz:<1024>] [/verboseFlag:<false>]
       [/doDump:<false>] [/statisticsFlag:<false>] [/help|?|h] [/version|v]


@argfile                 Read arguments from a file.
/srcIP:<value>           Specify Source IP Server Host.
/srcPort:<value>         Specify the Source Server Port to listen on.
/dstIP:<value>           Specify Destination IP Server Host - IP format.
/dstPort:<value>         Specify the Destination Port to forward to.
/maxConn <10>            Maximum Connections. (Default is "10")
/bufSz <1024>            Specify the Buffer Size in bytes. (Default is "1024")
/verboseFlag <false>     Specify Verbose logging (Default is "False")
/doDump <false>          Specify dumping of network packets (Default is
                         "False")
/statisticsFlag <false>  Specify Statistics (Default is "False")
/help                    Show usage.
/version                 Show version.

Figure 1. Snapshot of command line parameters available.

Whoa! What are these switches - ahhh....reminiscent of UNIX command line switches - and I love 'em. Ya gotta luv 'em or hate 'em. I will explain here what each command line switch does to make this article feel complete. Just to make it easier to work out how to use this program, if this program is sitting on the same PC as the firewall is running on, then, the source IP address would be that PC's IP address, which would be visible to the external world, the destination IP address would be somewhere on the internal network.

For example:

  • Internal network IP is: 192.169.20.x.
  • Firewall PC's IP address is 192.169.20.2.

    Make sure that you have unblocked an allocated port, allowing traffic in both directions, before proceeding! Let's say for example, designate a port 4321 so that Remoting clients will have the network address, or in Remoting parlance, the URI address which is in this case, 192.169.20.2:4321.

  • Internal Remoting server is at 192.169.20.10 running off port 1234.

The command line would be:

SrvrRedirector /srcIP:192.169.20.2 /srcPort:4321 /dstIP:192.169.20.10 /dstPort:1234
  • /maxConn: An integer size - is the maximum connections that the server will listen for. Default number of maximum connections is 10.
  • /bufSz: An integer size - is the size of the buffer to hold the incoming (external network data). Obviously, there needs to be a fine balance drawn here. If you specify a size too big, network communications would be fast but the PC's speed would slow down due to the allocation size of memory. If you specify a size too small, the network communications would be slow but the PC's speed would not be compromised. Default number is 1024 or 1 MB. I recommend 4096 as the buffer size.
  • /verboseFlag: bool - either true or false - if specified, it will output extra messages to the screen in this case. Default is false.
  • /doDump: bool - either true or false - an interesting one, I put this in, to prove to myself that the data is coming and going! Default is false.
  • /statisticsFlag: bool - either true or false - this will tell how many bytes were transferred etc. Default is false.

Unless ahem, that you have loads of memory, it should not matter what you specify for /bufSz. For instance, on my trustworthy reliable laptop (which is six years old and still going strong - a fine 366Mhz Pentium II with 256MB RAM), it is noticeable if I set the buffer size too big, a slow down would occur, especially when there're simultaneous connections!

So what are we waiting for....ahhh "The" Code

The code is completely self-contained in one file, aptly called, SrvrSock.cs. This file consists of many classes such as:

  • SrvrRedirectorCmdLine - an inherited class from Genghis' CommandLineParser class.
  • KickStarter - Need I say more?? It sets the whole wheel (or chain of bondage with .NET?!) in motion!
  • Helper - This contains one static method DumpBytes and one function IsHostDottedQuad.
  • SrvrRedirectorSocket - The core of traffic redirection!
  • Connection - Responsible for the asynchronous socket communication between external/internal traffic.
  • ConnectionDetails - This holds the details of each connection made to the server.
  • ServerMessagesEventArgs, StatisticsEventArgs and ServerDumpDataEventArgs - these are inherited from the System.EventArgs class, to customise the event handling/subscription.

The KickStarter...

Sing to the tune by a techno music band - The Prodigy, whose album is called 'The Fat of the Land' - I'm a ... FirestarterKickStarter...That's what caffeine does to me!! Wink | ;-) Okay, there's one function in here and that's Main, let's see what it looks like...as shown in figure 2.

[STAThread]
static void Main(string[] args){
  SrvrRedirectorCmdLine cl = new SrvrRedirectorCmdLine();
  int maxBufSz = -1;
  if (!cl.ParseAndContinue(args)) return;
  if (Helper.IsHostDottedQuad(cl.srcIP) && Helper.IsHostDottedQuad(cl.dstIP)){
    if (cl.srcPort == 0 || cl.srcPort < 1024){
      Console.WriteLine("Invalid Source Port. Must be > 1024!");
      return;
    }
    if (cl.dstPort == 0 || cl.dstPort < 1024){
      Console.WriteLine("Invalid Destination Port. Must be > 1024!");
      return;
    }
    if (cl.bufSz == 0 || cl.bufSz > int.MaxValue){
      maxBufSz = 1024;
    }else{
      maxBufSz = cl.bufSz;
    }
    Console.WriteLine("SETUP>> srcIPAddr = {0}:{1}. dstIPAddr = {2}:{3}.", 
                               cl.srcIP, cl.srcPort, cl.dstIP, cl.dstPort);
    Console.WriteLine("SETUP>> BUFFER Size: {0} bytes.", 
                  maxBufSz.ToString("###,###,###,###"));
    SrvrRedirectorSocket srvrSock = new SrvrRedirectorSocket(cl.srcIP, 
               cl.srcPort, cl.maxConn, cl.dstIP, cl.dstPort, maxBufSz, 
               cl.verboseFlag, cl.doDump, cl.statisticsFlag);
    if (cl.verboseFlag) SrvrRedirectorSocket.ServerMessages += new 
       SrvrRedirectorSocket.ServerMessagesEventHandler(
       SrvrRedirectorSocket_ServerMessages);
    if (cl.statisticsFlag) SrvrRedirectorSocket.Statistics += new 
       SrvrRedirectorSocket.StatisticsEventHandler(
       SrvrRedirectorSocket_Statistics);
    if (cl.doDump) SrvrRedirectorSocket.ServerDumpData += new 
       SrvrRedirectorSocket.ServerDumpDataEventHandler(
       SrvrRedirectorSocket_ServerDumpData);
    srvrSock.Connect();
    if (srvrSock.SrvrRedirectOk){
      Console.WriteLine("REDIR-SRVR>> Press any key to shutdown.");
      Console.Read();
    }
  }else{
    Console.WriteLine("Invalid IP Addresses for source/destination supplied");
    return;
  }
}

Figure 2. Main entry point of code.

There's a lot of simple, elementary validation on the IP addresses and port numbers to ensure they fall within the legitimate range. The line of interest is highlighted above in bold. Looks simple huh? Smile | :) The rest of the code after it, checks if the flags are set for verbosity, statistics and dumping, and subscribe to the events if the flags are specified. The code also checks if the sockets were set up okay, and sits there idly, waiting for some input on the keyboard which terminates the program.

Helper Class

There're two static blocks of code, one is a function IsHostDottedQuad and the other DumpBytes.

  • IsHostDottedQuad - This is a simple compiled regular expression to check if the host name is in dotted quad notation and returns true if it is valid, otherwise returns false.
  • DumpBytes - A very simplistic piece of code, shown below, in figure 3, that can be used in any circumstance. Initially I was using the string to build up the string of the data in hexadecimal notation, but it was damn slow, so I can verify and say that the StringBuilder class works much faster!
public static void DumpBytes(byte[] dumpdBlock, string sMsg, int dumpdBlockLen){
  int MaxChars = 16;
  int LineCount = 0;
  int byteCount = 0;
  int nLoopCnt = 0;
  StringBuilder sb = new StringBuilder();
  sb.Append(string.Format("=> Begin Dump. {0}; {1}\n", sMsg, 
             DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss")));
  while (byteCount != dumpdBlockLen){
    int StartCount = LineCount * MaxChars;
    sb.Append(string.Format("{0} ", StartCount.ToString("X4")));
    string sAsciiDump = string.Empty;
    for (nLoopCnt = 0; nLoopCnt < MaxChars; nLoopCnt++){
    if (dumpdBlock[byteCount] < 128){
      char ch = (char)dumpdBlock[byteCount];
      if (char.IsLetterOrDigit(ch) || char.IsPunctuation(ch)){
        sAsciiDump += char.ToString(ch);
      }else{
        sAsciiDump += ".";
      }
    }else{
      sAsciiDump += ".";
    }
    sb.Append(string.Format("{0} ", 
              dumpdBlock[byteCount++].ToString("X2")));
    if (byteCount == dumpdBlockLen) break;
   }
   if (nLoopCnt < MaxChars){
   for (int j = nLoopCnt + 1; j < MaxChars; j++){
    sb.Append("   ");
   }
  }
  sb.Append(string.Format("|{0}\n", sAsciiDump));
  LineCount++;
 }
 sb.Append("=> End Dump.\n");
 SrvrRedirectorSocket.OnServerDumpDataEventHandler(new 
              ServerDumpDataEventArgs(sb.ToString()));
}

Figure 3. DumpBytes helper method.

xxxxxEventArgs

These classes are derived or inherited from the System.EventArgs class, and is used for the event handlers which are static and is globally accessible from within the SrvrRedirectorSocket class.

public delegate void ServerMessagesEventHandler(object sender, 
                                   ServerMessagesEventArgs e);
public static event ServerMessagesEventHandler ServerMessages;
public delegate void StatisticsEventHandler(object sender, 
                                   StatisticsEventArgs e);
public static event StatisticsEventHandler Statistics;
public delegate void ServerDumpDataEventHandler(object sender, 
                                   ServerDumpDataEventArgs e);
public static event ServerDumpDataEventHandler ServerDumpData;

Figure 4. Events and delegates found within SrvrRedirectorSocket.

I have made the event handler signatures the same as any other events such as those found in the WinForms section, for instance, button1_Click(object sender, System.EventArgs e). This makes it easier to distinguish them from the actual class' methods.

ConnectionDetails

A vital part that plays in redirecting network traffic, it holds such details as internal and external sockets etc. I will give a brief summary of properties etc. highlighted below, in figure 5.

#region ConnectionDetails Class
public class ConnectionDetails : IDisposable{
  private Socket extSocket = null;
  private Socket intSocket = null;
  private IPEndPoint extEp = null;
  private byte[] bBufExt = null;
  private byte[] bBufInt = null;
  private bool bIntSocketOk = false;
  private long lBytesSent = 0;
  private long lBytesReceived = 0;
  private DateTime dtBeginRedirecting;
  private DateTime dtEndRedirecting;
  private bool disposed = false;
  
  public ConnectionDetails(){}

  ~ConnectionDetails(){
     Dispose(true);
  }

  public Socket ConnectionSocketExternal{
     get{ return this.extSocket; }
     set{ this.SetUpConnectionExternalEndPoint(value); }
  }

  public Socket ConnectionSocketInternal{
     get{ return this.intSocket; }
  }

  public IPAddress ConnectionExternalAddress{
     get{ return this.extEp.Address; }
  }

  public int ConnectionExternalPort{
     get{ return this.extEp.Port; }
  }

  public byte[] ConnectionExternalBuffer{
    get{ return this.bBufExt; }
    set{ this.bBufExt = value; }
  }

  public byte[] ConnectionInternalBuffer{
    get{ return this.bBufInt; }
    set{ this.bBufInt = value; }
  }

  public long ConnectionBytesSent{
    get{ return this.lBytesSent; }
    set{ this.lBytesSent = value; }
  }
  
  public long ConnectionBytesReceived{
    get{ return this.lBytesReceived; }
    set{ this.lBytesReceived = value; }
  }

  public bool ConnectionSocketInternalOk{
    get{ return this.bIntSocketOk; }
  }

  public DateTime ConnectionBegan{
    get{ return this.dtBeginRedirecting; }
    set{ this.dtBeginRedirecting = value; }
  }
 
  public DateTime ConnectionEnded{
    get{ return this.dtEndRedirecting; }
    set{ this.dtEndRedirecting = value; }
  }

  private void SetUpConnectionExternalEndPoint(Socket _sock){
    this.extSocket = _sock;
    this.extEp = (IPEndPoint)this.extSocket.RemoteEndPoint;
  }

  public bool OpenInternalConnection(string DestIPAddr, int DestIPPort){
    try{
      string hostName = DestIPAddr.ToLower();
      IPHostEntry localMachineInfo = null;
      try{
        if (Helper.IsHostDottedQuad(hostName)){
           localMachineInfo = Dns.GetHostByAddress(hostName);
        }else{
           localMachineInfo = Dns.Resolve(hostName);
        }
      }catch(SocketException){
        SrvrRedirectorSocket.OnServerMessagesEventHandler(new 
          ServerMessagesEventArgs(string.Format(">> Unable to resolve" + 
                                      " {0}. Terminating.", hostName)));
        return false;
      }
      int nHostAddressIndex = 0;
      if (localMachineInfo.AddressList.Length > 1 && 
                 Helper.IsHostDottedQuad(hostName)){
         for (int nLoopCnt = 0; nLoopCnt < 
              localMachineInfo.AddressList.Length; nLoopCnt++){
            if (localMachineInfo.AddressList[nLoopCnt].ToString().
                                                Equals(hostName)){
              nHostAddressIndex = nLoopCnt;
              break;
            }
         }
      }
      IPEndPoint myEndPoint = new 
        IPEndPoint(localMachineInfo.AddressList[nHostAddressIndex], 
        DestIPPort);
      this.intSocket = new Socket(myEndPoint.Address.AddressFamily, 
        SocketType.Stream, ProtocolType.Tcp);
      this.intSocket.Connect(myEndPoint);
      this.bIntSocketOk = true;
   }catch(SocketException sockEx){
      if (sockEx.NativeErrorCode == 10053 || sockEx.NativeErrorCode == 10054){
      }else{
         if (sockEx.NativeErrorCode == 10061){
            SrvrRedirectorSocket.OnServerMessagesEventHandler(new 
                ServerMessagesEventArgs(string.Format("ERR>> Could not " + 
                "connect to [{0}:{1}]", DestIPAddr, DestIPPort)));
         }else{
            SrvrRedirectorSocket.OnServerMessagesEventHandler(new 
                ServerMessagesEventArgs(string.Format("ERR>> Socket " + 
                "Exception @ OpenInternalConnection(...); #[{0}] Message: {1}", 
                sockEx.NativeErrorCode, sockEx.Message)));
         }
      }
      this.bIntSocketOk = false;
   }catch(Exception eX){
      SrvrRedirectorSocket.OnServerMessagesEventHandler(new 
         ServerMessagesEventArgs(string.Format("ERR> Exception @" + 
         " OpenInternalConnection(...); Message: {0}", eX.Message)));
      this.bIntSocketOk = false;
   }
   return this.bIntSocketOk;
  }

  public override bool Equals(object obj) {
     ConnectionDetails rhs = obj as ConnectionDetails;
     if (rhs != null){
        if ((this.ConnectionExternalAddress == rhs.ConnectionExternalAddress) && 
           (this.ConnectionExternalPort.Equals(rhs.ConnectionExternalPort)) &&
           (this.ConnectionSocketExternal.Equals(rhs.ConnectionSocketExternal))){
              return true;
        }else{
              return false;
        }
     }
     return false;
  }

  public override int GetHashCode() {
    return this.GetHashCode ();
  }

  public override string ToString() {
    return string.Format("[{0}:{1}]", 
           this.extEp.Address.ToString(), this.extEp.Port);
  }

  public void Dispose(){
    Dispose(true);
    GC.SuppressFinalize(this);
  }
 
  protected virtual void Dispose(bool disposing) {
    // Check to see if Dispose has already been called.
    if(!this.disposed) {
      if (this.intSocket != null){
        this.intSocket.Shutdown(SocketShutdown.Both);
        this.intSocket.Close();
      }
      if (this.extSocket != null){
        this.extSocket.Shutdown(SocketShutdown.Both);
        this.extSocket.Close();
      }
      this.extEp = null;
      this.bBufExt = null;
      this.bBufInt = null;
      // If disposing equals true, dispose all managed 
      // and unmanaged resources.
      if(disposing) {
      }
    }
    disposed = true;         
  }
}

Figure 5. Auxiliary features required for each incoming connection to the server.

Properties

  • ConnectionSocketExternal - Both read/write accessor to assign the external socket to this variable: this.extSocket.
  • ConnectionSocketInternal - A readonly accessor, since the internal socket is created and not exposed publicly.
  • ConnectionExternalAddress - A readonly accessor, we do not have a need to manipulate this.extEP.Address.
  • ConnectionExternalPort - A readonly accessor, this returns a port number which is an int type.
  • ConnectionExternalBuffer and ConnectionInternalBuffer - Both read/write accessors which act as the buffer to pass data from external to internal and vice-versa.
  • ConnectionBytesSent and ConnectionBytesReceived - Again, both read/write accessor to keep track of how many bytes passed through in both directions. This is used by the Statistics event handler.
  • ConnectionSocketInternalOk - A readonly accessor which returns a bool to determine if the internal connection was initialised properly. It can return false due to a host of obscure reasons such as the destination/internal IP address is not working or no code listening in on that destination or internal IP address.
  • ConnectionBegan and ConnectionEnded - Both read/write accessor which use the DateTime type, purely to keep track of when the external or incoming connection was made and when it ended.

Methods/Functions

  • SetUpConnectionExternalEndPoint - Simply sets up the external end-point so that we know where it is coming from.
  • OpenInternalConnection - This takes in a destination IP address (which is of string type) and gets validated by Helper.IsHostDottedQuad. Then it attempts to resolve the IP address via Dns.Resolve method, binds to the designated destination or internal IP address, and returns true if the internal connection setup was successful, otherwise false for failure.
  • Equals, GetHashCode and ToString - these are overridden methods used for doing comparison checks. Notice, how the ToString formats the overall class into one neat IP address represented by the external IP address followed by the port number, [xx.xx.xx.xx:yyyy].
  • Dispose - Hmmm.. tricky one this is, as this class implements the IDisposable interface so that I can easily destroy the sockets by an instance of this class, e.g. some_instance_of_ConnectionDetails.Dispose() which cleans up the sockets. But then again, the implementation of using Dispose is my weakness!!

Connection

This makes use of asynchronous sockets operations. For brevity, as this class is long, I'll highlight parts of the code that would be of interest here.

  • Constructors - there're two constructors that deal with the instantiation of this class, with different signatures:
    • Connection(Socket sock, int port, bool verboseFlag, bool doDump) - This is instantiated automatically upon the initialisation of the server socket's Connect method found within the SrvrRedirectorSocket class. The other constructor, hmmm, I think that was a legacy piece of code during development/testing and can be dismissed! :-P (My fault - I am to blame myself as much as the source control revision! <g>)
    • Connection(string hostName, int port) - This takes care of taking a hostname which can and should be Dns.Resolveable.
  • Properties - There're two properties, TargetConnectionIPAddr and TargetConnectionPort which are both read/write, and holds the IP address of the destination or the internal IP address.

    Now, this is where things get interesting. As I've pointed out earlier, this class makes use of asynchronous sockets, thanks to the article in MSDN (see reference below), it is truly very fast and furthermore, the code appears to be very clean as I don't have to deal with threading - hey, let the CLR (Common Language Runtime) take care of threading - a huge load taken off my shoulders! Smile | :) Here's the method that takes care of it all, as shown in figure 6:

    public void cbHandleIncomingExternalConnection(IAsyncResult result){
      ConnectionDetails conn = new ConnectionDetails();
      try{
        Socket s = (Socket)result.AsyncState;
        conn.ConnectionSocketExternal = s.EndAccept(result);
        conn.ConnectionExternalBuffer = new 
          byte[SrvrRedirectorSocket._StaticBufferSize];
        conn.ConnectionInternalBuffer = new 
          byte[SrvrRedirectorSocket._StaticBufferSize];
        conn.ConnectionBegan = DateTime.Now;
        SrvrRedirectorSocket.OnServerMessagesEventHandler(new 
            ServerMessagesEventArgs(string.Format("CON> {0} connected on {1}.", 
            conn.ToString(), DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"))));
        if (conn.OpenInternalConnection(this.sRedirectHost, this._redirPort)){
          // TAKE NOTE HERE! This was mentioned previously above (Figure 5)
          conn.ConnectionSocketExternal.BeginReceive(conn.ConnectionExternalBuffer, 
               0, conn.ConnectionExternalBuffer.Length, SocketFlags.None, 
               new AsyncCallback(this.cbHandleIncomingExternalTraffic), conn);
          conn.ConnectionSocketInternal.BeginReceive(conn.ConnectionInternalBuffer, 
               0, conn.ConnectionInternalBuffer.Length, SocketFlags.None, 
               new AsyncCallback(this.cbHandleIncomingInternalTraffic), conn);
        }else{
          return;
        }
        SrvrRedirectorSocket._StaticSockRedir.BeginAccept(new 
          AsyncCallback(this.cbHandleIncomingExternalConnection), 
          result.AsyncState);
      }catch(SocketException sockEx){
        if (sockEx.NativeErrorCode == 10053 || sockEx.NativeErrorCode == 10054){
          SrvrRedirectorSocket.CloseConnection(conn);
        }else{
          SrvrRedirectorSocket.OnServerMessagesEventHandler(new 
            ServerMessagesEventArgs(string.Format("ERR> Socket Exception @" + 
            " cbHandleIncomingConnection(...); Message: {0}", 
            sockEx.Message)));
        }
      }catch(System.InvalidOperationException){
        SrvrRedirectorSocket.CloseConnection(conn);
      }catch(Exception){
        SrvrRedirectorSocket.CloseConnection(conn);
        SrvrRedirectorSocket.OnServerMessagesEventHandler(new 
          ServerMessagesEventArgs(string.Format("CON> {0} connected on {1} (UTC).", 
          conn.ToString(), DateTime.Now.ToUniversalTime())));
      }
    }

    Figure 6. Asynchronous method to handle the incoming connections to the server.

    The line with the comment highlighted in bold above, is where the wheels turn. Remember that this.sRedirectHost is taken from the command line, ditto for this._redirPort, and hence, upon connection initiated from an external source, it attempts to connect up to the destination or internal IP address. If conn.OpenInternalConnection is successful, it will then start to do an asynchronous receive on both the external and internal sockets, which are implemented as call-backs. cbHandleIncomingExternalTraffic and cbHandleIncomingInternalTraffic, are near-identical and simply relay/redirect traffic. Upon receiving data from the external source, it performs an synchronous socket Send to the internal address - connDets.ConnectionSocketInternal.Send. Likewise, upon receiving data from the internal source, it performs an synchronous socket Send to the external address connDets.ConnectionSocketExternal.Send.

    How does conn get into the body of the code within the call-backs cbHandleIncoming[External|Internal]Traffic? Look more closely, at the call-back in handling incoming external connections. We've instantiated ConnectionDetails prior to the try{...}catch(...) block. It is that parameter that gets passed in as the last parameter to the socket's BeginReceive asynchronous call. When that call-back code gets executed, we simply cast the IAsyncResult parameter to the type ConnectionDetails and hey presto, as if by magic, we can then access the details held in that class. And continue processing as normal. Clever stuff this asynchronous methods do?! Smile | :)

    SrvrRedirectorSocket

    This class is a high level implementation of the rest of the sections that I have highlighted in the above, encapsulating all of the above into one nice class. This class is exposable from the Main(...) startup and is where the command line parameters (courtesy of Genghis' Command Line Parser class) gets passed into this class via its constructor.

    • Connect - This method sets up the main listening socket or server socket, if you like, and creates a connection backlog queue of n connections as per the command line parameter /maxConn:n and calls the server socket's asynchronous method BeginAccept.
    • ShutDown - This method simply shuts down the server socket.
    • OnServerMessagesEventHandler, OnStatisticsEventHandler, OnServerDumpDataEventHandler - Event handlers that print out the messages, dumped data and statistics respectively.

    The following code snippet, as shown in figure 7, shows the method that closes down an external socket, and prints out the respective statistics, if it is specified on the command line. The calculations below seem a little bit approximate. The way I have computed the bytes per second is this:

    • X bytes divided by Y where X is the amount of bytes passed through from the external source to internal/destination IP address and Y is the duration in seconds since the external source first connected up.
    • X bytes gets divided by 1024 to get kilobytes per second...hand on my heart, I am not sure if this is accurate. Please feel free to make any suggestions/criticisms! Smile | :)
    public static void CloseConnection(ConnectionDetails connDets){
      try{
        connDets.ConnectionEnded = DateTime.Now;
        if (_bStatistics){
          OnStatisticsEventHandler(new StatisticsEventArgs(string.Format("STA>" + 
             " {0} Bytes Received: {1}; Bytes Sent: {2}", connDets.ToString(), 
             connDets.ConnectionBytesReceived.ToString("#,###,###,###,###"), 
             connDets.ConnectionBytesSent.ToString("#,###,###,###,###"))));
          TimeSpan difference = connDets.ConnectionEnded - 
                                connDets.ConnectionBegan;
          long bytesSentPerSec = ((long)(connDets.ConnectionBytesSent / 
                                             difference.TotalSeconds));
          long bytesRecvdPerSec = ((long)(connDets.ConnectionBytesReceived / 
                                                  difference.TotalSeconds));
          decimal kBytesSentPerSec = ((decimal)(bytesSentPerSec / 1024));
          decimal kBytesRecvdPerSec = ((decimal)(bytesRecvdPerSec / 1024));
          OnStatisticsEventHandler(new StatisticsEventArgs(string.Format("STA> " + 
             "{0} - Connection Uptime: {1} seconds\nSTA> Bytes Received/sec: " + 
             "{2} (KB/sec: {3})\nSTA> Bytes Sent/sec: {4} (KB/sec: {5})", 
             connDets.ToString(), difference.TotalSeconds, 
             bytesRecvdPerSec.ToString("#,###,###,###,###"), 
             kBytesRecvdPerSec.ToString("#,###,###,##0.0000"), 
             bytesSentPerSec.ToString("#,###,###,###,###"), 
             kBytesSentPerSec.ToString("#,###,###,##0.0000"))));
        }
        OnServerMessagesEventHandler(new 
             ServerMessagesEventArgs(string.Format("CON> " + 
             "{0} Closed on {1}.", connDets.ToString(), 
             connDets.ConnectionEnded.ToString("dd/MM/yyyy HH:mm:ss"))));
        OnServerMessagesEventHandler(new ServerMessagesEventArgs("=> Goodbye. :)"));
        connDets.Dispose();
      }catch(SocketException sockEx){
        OnServerMessagesEventHandler(new ServerMessagesEventArgs(
             string.Format("ERR> Socket Exception @ " + 
             "CloseConnection(...); Message: {0}", sockEx.Message)));
      }catch(ObjectDisposedException){
      }catch(InvalidOperationException){
      }catch(Exception){
      }
    }

    Figure 7. Method to close down an incoming connection (from the external source) to the server.

    Points of Interest

    • If you're using strings to concatenate pieces of data - go for the StringBuilder class and use it. The benefits and performance outweighs it! Wink | ;-)
    • Check out the Winsock FAQ for a mine of information - that's a great place to learn about the good ol' fashioned Winsock, which would be of help in finding out how sockets under .NET works. The beauty is, it has a zipped archive of its web pages for reading off-line!
    • I know, that using finalizers/destructors is an iffy one, that's due to my limitation in understanding the concept as it seems a bit bizarre. If you have any input, I'd be more than delighted to hear what you would have to say on this in relation to the ConnectionDetails class.
    • When dealing with sockets, be sure to make use of NativeErrorCode which is part of the SocketException class. This corresponds to the actual WinSock's error codes, the ones of interest are:
      • 10053/WSAECONNABORT - Socket operation was aborted.
      • 10054/WSAECONNRESET - Socket connection was reset.
      • 10061/WSAECONNREFUSED - Socket connection was refused.

      By the way, the WSAXXXXX are the define's taken from the WinSock header file that is used within Win32 API 'C' programming, and can be found within Winsock2.h in the PSDK (Platform SDK). Anything else is considered a problem!

    • The statistics are not entirely accurate as I was not sure of the calculations to work out how much bytes are transferred per second - the result looks a bit iffy. I also found myself getting a tad confused about how many bytes were being sent and received, as to whether the count came from the external source or the internal source, so take that with a pinch of salt! Smile | :)
    • I have yet to install Linux Gentoo on my laptop, and when I do, hopefully within the next few days, I'll install Mono and recompile this code without changes and update this article in due course! This should be fun! Smile | :)

    There, you have it, I hope you enjoyed this article and learnt a thing or two from it, much in the same way as I did. Please feel free to contact me for suggestions, criticisms, at the forum below this article, or email me at tomas_o_braonain [at] hotmail [dot] com. (Spam will be Dispose'd off using my deterministic and finalizer destructive function!) Thank you for reading this article and also hope it would be of use to you! Smile | :) Happy .NET'ting. Smile | :)

    References

    • MSDN, August 2005, 'Winsock - Get closer to the wire with high-performance sockets in .NET', Daryn Kiely, Pg 81.
    • Ingo 'Remoting Hacker' Rammer - BiDirTcp - Open Sourced Bi-Directional TCP Channel, Thinktecture.com.
    • Genghis - Genghis Group Website, can also be found on GotDotNet Website.
    • Genuine Channels - Genuine Channels - go on, give them a shot at their work - truly amazing!

    History

    • Oct 21, 2005 - Initial version.

    License

    This article, along with any associated source code and files, is licensed under A Public Domain dedication

    Share

    About the Author

    Tomas Brennan
    Software Developer (Senior)
    Ireland Ireland
    B.Sc. in Information Systems.
    Languages: C, Assembly 80x86, VB6, Databases, .NET, Linux, Win32 API.
     
    Short Note:
    Having worked with IT systems spanning over 14 years, he still can remember writing a TSR to trap the three-finger salute in the old days of DOS with Turbo C. Smile | :) Having worked or hacked with AS/400 system while in his college days, graduating to working with Cobol on IBM MVS/360, to RS/6000 AIX/C. He can remember obtaining OS/2 version 2 and installing it on an antique 80386. Boy it ran but crawled! Smile | :) Keen to dabble in new technologies. A self-taught programmer, he is keen to relinquish and acquire new bits and bytes, but craves for the dinosaur days when programmers were ultimately in control over the humble DOS and hacking it!! Smile | :)

    Comments and Discussions

     
    GeneralUnable to resolve XXX.XXX.XXX.XXX. Terminating [modified] PinmemberCbadboy15-Jun-10 15:05 
    GeneralUpdate... PinmemberTomas Brennan23-Apr-09 10:27 
    General[Message Deleted] Pinmemberit.ragester2-Apr-09 21:57 
    GeneralRe: sip PinmemberTomas Brennan3-Apr-09 2:37 
    QuestionFPipe Pinmemberpiers71-Nov-05 2:37 
    AnswerRe: FPipe PinmemberTomas Brennan5-Nov-05 2:31 
    GeneralPort Redirection Pinmembersides_dale29-Oct-05 18:20 
    GeneralRe: Port Redirection PinmemberTomas Brennan31-Oct-05 4:11 
    GeneralRe: Port Redirection Pinmembersides_dale7-Nov-05 15:19 
    GeneralRe: Port Redirection PinmemberTomas Brennan9-Nov-05 3:23 
    GeneralRe: Port Redirection Pinmembersides_dale12-Nov-05 17:12 
    GeneralWelcome! PinmemberTomas Brennan22-Oct-05 3:15 
    NewsLess heavyweight commandline parser Pinmemberleppie21-Oct-05 13:00 
    GeneralRe: Less heavyweight commandline parser PinmemberTomas Brennan22-Oct-05 2:52 
    GeneralCommandLineOptions class Pinmemberpiers71-Nov-05 2:50 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

    | Advertise | Privacy | Mobile
    Web04 | 2.8.140926.1 | Last Updated 21 Oct 2005
    Article Copyright 2005 by Tomas Brennan
    Everything else Copyright © CodeProject, 1999-2014
    Terms of Service
    Layout: fixed | fluid