玄铁剑

成功的途径:抄,创造,研究,发明...
posts - 128, comments - 42, trackbacks - 0, articles - 174

Windows TCP Tunnel

Posted on 2007-01-03 11:05 玄铁剑 阅读(770) 评论(0)  编辑 收藏 引用 所属分类: windows

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.

Collapse
				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);
}
  
publicstaticvoid 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.

Collapse
				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.

Collapse
				public
				void run()
{
    //validate that both the client side and server //side sockets are ok. If so, do read/writeif (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.

Han_Jun_Li


Click here to view Han_Jun_Li's online profile.

只有注册用户登录后才能发表评论。