Click here to Skip to main content
6,594,932 members and growing! (13,914 online)
Email Password   helpLost your password?
General Programming » Internet / Network » Client/Server Development     Intermediate

Windows TCP Tunnel

By Han_Jun_Li

This project shows how to forward TCP sockets from one machine to another.
C#, Windows, .NET 1.1VS.NET2003, Dev
Posted:29 Jun 2006
Updated:12 Jul 2006
Views:33,237
Bookmarked:29 times
Announcements
Loading...
 
Search    
Advanced Search
Add to IE Search
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
8 votes for this article.
Popularity: 2.86 Rating: 3.17 out of 5
2 votes, 25.0%
1
1 vote, 12.5%
2

3
1 vote, 12.5%
4
4 votes, 50.0%
5

Introduction

Microsoft .NET contains many enhanced foundation classes that makes writing high performance socket applications easy. This project demonstrates how to use asynchronous sockets to write a high performance yet simple to use TCP tunneling application--mapping ports from one machine to many others.

I was looking for a simple TCP tunneling application but didn't find any on this site. My requirement is a high performance (low CPU utilization) application, that can run as a Windows Service, is easy to configure, and can handle many concurrent ports.

Overview

Program/Class Design

Using the code

Since WinTunnel is a Windows Service, the first step is to install it. You can pass a command line switch -install to install it as a service using the LocalSystem account. Optionally, you can pass in a user name and password via the -user and -password switches, respectively.

The program needs to read a configuration file called WinTunnel.ini in the current directory or the directory where the binary is located. The content of the configuration file is very simple. The first line defines a service name--can be anything because it is only used for printing debug messages. The second line defines where to listen for client connections. The third line defines the server connection information.

[HTTP]
accept  = 80               <====== defines which port to accept the 
                                   client connection--
                                   either <Port> or <IP:Port>
connect = 192.168.1.10:80  <====== defines the target of the connection
                                   --must be in the format <IP:Port>

[HTTPS]
accept  = 192.168.1.100:443
connect = 192.168.1.125:443

Interesting issues

One interesting problem I encountered is that .NET requires the use of InstallUtil.exe to put the C# service into the Global Assembly Cache (GAC) and register with the Service Control Manager (SCM). In order to just install it by running the binary, a background process is launched that calls InstallUtil.exe with all the required parameters. The output is then redirected to a buffer and printed out on the console--see the code below.

static void launchProcess(String binary, String argument)
{
    System.Diagnostics.ProcessStartInfo psInfo = 
     new System.Diagnostics.ProcessStartInfo(binary, argument);

    System.Console.WriteLine();
    System.Console.WriteLine(psInfo.FileName + 
                             " " + psInfo.Arguments);
    System.Console.WriteLine();

    psInfo.RedirectStandardOutput = true;
    psInfo.WindowStyle = 
           System.Diagnostics.ProcessWindowStyle.Hidden;
    psInfo.UseShellExecute = false;
    System.Diagnostics.Process ps;
    ps = System.Diagnostics.Process.Start(psInfo);
    System.IO.StreamReader msgOut = ps.StandardOutput;
    ps.WaitForExit(5000); //wait up to 5 seconds 

    if (ps.HasExited)
    {
        //write the output

        System.Console.WriteLine(msgOut.ReadToEnd());
    }
    return;
}

In order to debug a Service application, a debug switch was added to the application. Generally, it is hard to debug a service application because there is no console. When the debug switch is set, log messages are printed on the console. In order to properly shutdown the service application in this mode, a Win32 API call is needed to capture the Ctrl-C key press. Once it is captured, an event object is signaled to properly shutdown--see ConsoleEvent.cs for details.

//create a signal handler to detect Ctrl-C to stop the service

if (m_debug)
{
    m_ctrl = new ConsoleCtrl();
    m_ctrl.ControlEvent += new 
       ConsoleCtrl.ControlEventHandler(consoleEventHandler);
}
  
public static void consoleEventHandler(ConsoleCtrl.ConsoleEvent consoleEvent)
{
    if (ConsoleCtrl.ConsoleEvent.CTRL_C == consoleEvent)
    {
        Logger.getInstance().info("Received CTRL-C from" + 
                                  " Console. Shutting down...");
        WinTunnel.shutdownEvent.Set();
    }
    else
    {
        Logger.getInstance().warn("Received unknown" + 
               " event {0}.  Ignoring...", consoleEvent);
    }
}

Code discussion

ThreadPool

ThreadPool

In order to make the application design simple, the ThreadPool class was created. It allows the application to be broken down to various tasks and executed by the Thread Pool. This design greatly reduces the need to synchronize--a big performance bottleneck. The idea of the thread pool is that an application should not create many threads. Instead, a small number of them should be created during startup, and reused over and over again until shutdown. Any work that needs to be done in the application will have to be added to a task list, and if there is a free thread, it should pick up the task and execute it.

A good analogy would be with the UPS delivery service. It will be terribly in-efficient if a new worker has to be hired every time a package needs to be delivered. Instead, UPS hires a bunch of them and asks them to deliver packages over and over again.

Similar to the package in the above analogy, a piece of work that needs to be done in the application is called a task. The task must implement the ITask interface--which has two methods: getName() and run(). The getName() method is used for debugging and returns the details of the task. The run() method is the entry point for a thread in the thread pool to perform the work.

public interface ITask 
{
    void run();
    String getName();
}

In this application, there are only three types of tasks. The first task is to listen for client connections. This is implemented by the ProxyClientListenerTask. When the task executes, it creates a socket, and then calls the asynchronous BeginAccept() method. The call requires a callback method and also an object as a parameter (this).

public void run()
{
    listenSocket = new Socket( AddressFamily.InterNetwork, 
                               SocketType.Stream, ProtocolType.Tcp);
    listenSocket.Bind(m_config.localEP);
    listenSocket.Listen(10); //allow up to 10 pending connections

    logger.info("[{0}] Waiting for client connection at {1}...", 
                m_config.serviceName, m_config.localEP.ToString());

    listenSocket.BeginAccept( new AsyncCallback(
                    ProxyClientListenerTask.acceptCallBack), this);
}

The callback is invoked when the client connects to the listening socket. The first thing is to retrieve the passed in object which gets the socket. EndAccept() is then called, and the second task is created.

public static void acceptCallBack(IAsyncResult ar)
{
    ProxyConnection conn = null;
    try
    {
        ProxyClientListenerTask listener = 
                 (ProxyClientListenerTask) ar.AsyncState;

        //create a new task for connecting to the server side.

        conn = m_mgr.getConnection();
        conn.serviceName = listener.m_config.serviceName;
        //accept the client connection

        conn.clientSocket = listener.listenSocket.EndAccept(ar);

        logger.info("[{0}] Conn#{1} Accepted new connection." + 
                    " Local: {2}, Remote: {3}.", 
            conn.serviceName,
            conn.connNumber,
            conn.clientSocket.LocalEndPoint.ToString(), 
            conn.clientSocket.RemoteEndPoint.ToString() );

        conn.serverEP = listener.m_config.serverEP;
                
        //Start listening for connection on this port again

        listener.listenSocket.BeginAccept( new AsyncCallback(
          ProxyClientListenerTask.acceptCallBack), listener);

        //now try to connect to the server

        ProxyServerConnectTask serverTask = 
                    new ProxyServerConnectTask(conn);
        ThreadPool.getInstance().addTask(serverTask);
    } 
    catch (SocketException se)
    {
        logger.error("[{0}] Conn# {1} Socket Error occurred" + 
                     " when accepting client socket. Error Code is: {2}",
                     conn.serviceName, conn.connNumber, se.ErrorCode);
        if (conn != null)
        {
            conn.Release();
        }
    }
    catch (Exception e)
    {
        logger.error("[{0}] Conn# {1} Error occurred when" + 
            " accepting client socket. Error is: {2}",
            conn.serviceName, conn.connNumber, e);
        if (conn != null)
        {
            conn.Release();
        }
    }
    finally
    {
        conn = null; //free reference to the object

    }
}

The second task is to connect to the server that the client's connection should be mapped to. This is implemented by the ProxyServerConnectTask. When the task executes, it creates a new socket, and makes an asynchronous BeginConnect() to connect to the server.

public void run()
{
    m_conn.serverSocket = new Socket( AddressFamily.InterNetwork, 
                                      SocketType.Stream, ProtocolType.Tcp);
    m_conn.serverSocket.BeginConnect(m_conn.serverEP, 
         new AsyncCallback(connectCallBack), m_conn);    
}

The callback is invoked when the connection to the server is established and the third task is created.

public static void connectCallBack(IAsyncResult ar)
{
    ProxyConnection conn = (ProxyConnection) ar.AsyncState;

    try
    {
        conn.serverSocket.EndConnect(ar); 
                        
        logger.info("[{0}] ProxyConnection#{1}--connected " + 
            "to Server.  Server: {2}, Local: {3}.",
            conn.serviceName,
            conn.connNumber,
            conn.serverSocket.RemoteEndPoint, 
            conn.serverSocket.LocalEndPoint);

        //create task for proxying data between 

        //the client and server socket

        ProxySwapDataTask dataTask  = new ProxySwapDataTask(conn);
        ThreadPool.getInstance().addTask(dataTask);
    } 
    {...removed error handling code...}
}

The third task takes both the client socket and the server socket, and swaps data back and forth between them. This is implemented by the ProxySwapDataTask. There is a callback sending and receiving data for both the client and the server socket. There are also various checks to detect socket errors. When an error occurs, both the client and the server socket are shutdown.

public void run()
{
    //validate that both the client side and server 

    //side sockets are ok. If so, do read/write

    if (m_conn.clientSocket == null || m_conn.serverSocket == null)
    {
        logger.error("[{0}] ProxyConnection#{1}--Either" + 
                     " client socket or server socket is null.",
                     m_conn.serviceName, m_conn.connNumber);
        m_conn.Release();
        return;
    }

    if (m_conn.clientSocket.Connected && m_conn.serverSocket.Connected)
    {
        //Read data from the client socket

        m_conn.clientSocket.BeginReceive( m_conn.clientReadBuffer, 
               0, ProxyConnection.BUFFER_SIZE, 0,
               new AsyncCallback(clientReadCallBack), m_conn);
            
        //Read data from the server socket

        m_conn.serverSocket.BeginReceive( m_conn.serverReadBuffer, 
               0, ProxyConnection.BUFFER_SIZE, 0,
               new AsyncCallback(serverReadCallBack), m_conn);
    }
    else
    {
        logger.error("[{0}] ProxyConnection#{1}: Either" + 
            " the client or server socket got disconnected.", 
            m_conn.serviceName, m_conn.connNumber );
        m_conn.Release();
    }
    m_conn = null;
}

There is a callback for sending and receiving data for both the client and the server socket. There are also various checks to detect socket errors. When an error occurs, the connection is shutdown, and both the client and the server socket are closed.

End note

Unlike others, this project is the complete source of a fully functioning application. As such, there are various classes such as logging that I don't even cover in the discussion. I only try to point out the interesting things that I encountered when designing and implementing the application. Hopefully, it will be useful for someone doing something similar.

History

  • 2006-06-28 - Initial implementation.
  • 2006-07-10 - Enhanced to show more debug messages, added more error handling logic, and cleanup of objects. Also updated this documentation.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Han_Jun_Li


Member

Location: United States United States

Other popular Internet / Network articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 10 of 10 (Total in Forum: 10) (Refresh)FirstPrevNext
GeneralTunnel via HTTP? PinmemberMarco Kummer2:23 26 Aug '09  
GeneralUDP PinmemberCotizoS4:11 4 Jul '09  
QuestionLog window PinmemberDott. Marco Zaino9:13 17 Dec '08  
GeneralBug and its stack trace PinmemberMember 43231505:38 7 Oct '08  
GeneralBug Pinmember[JoE]11:01 12 Sep '08  
GeneralTCP ports forwarding in a little different way PinmemberAlex Kucherenko1:46 27 Apr '08  
GeneralExcellent Pinmemberm_pet8:04 16 Oct '07  
GeneralAwesome Pinmembermail57235216:18 14 Mar '07  
QuestionAuthentication Pinmemberalex_fetbroyt23:00 13 Sep '06  
GeneralInstallUtils PinmemberBill Seddon6:55 3 Sep '06  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 12 Jul 2006
Editor: Smitha Vijayan
Copyright 2006 by Han_Jun_Li
Everything else Copyright © CodeProject, 1999-2009
Web15 | Advertise on the Code Project