Search code examples
c#sslhttp-proxyhttp-tunneling

Http tunnel does not work for some websites


I have written a simple http tunnel proxy server:

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace TrafficMonitor
{
    public class ProxyServer
    {
        const int BufferSize = 255;
        public int Port { get; set; } = 9999;
        private static Regex httpRegex = new Regex(@"^(?<method>[a-zA-Z]+)\s(?<url>.+)\sHTTP/(?<major>\d)\.(?<minor>\d+)$");

        public async Task RunServerForEver()
        {
            var listner = new TcpListener(IPAddress.Any, Port);
            listner.Start();
            Console.WriteLine($"Listening on port {Port}");
            try
            {
                while (true)
                {
                    var client = await listner.AcceptTcpClientAsync();
                    ThreadPool.QueueUserWorkItem(HandleClient, client);
                }
            }
            finally
            {
                listner.Stop();
                Console.WriteLine("Server stopped");
            }
        }

        void HandleClient(object state)
        {
            TcpClient client = (TcpClient)state;
            Console.WriteLine($"New Connection Received: #{client.Client.Handle}");
            try
            {
                byte[] bytes = new byte[BufferSize];
                using (client)
                using (var stream = client.GetStream())
                {
                    int i = stream.Read(bytes, 0, bytes.Length);
                    var data = Encoding.ASCII.GetString(bytes, 0, i);
                    Console.WriteLine("Received: {0}", data);
                    if (!ParseHostPort(data, out string host, out int port))
                    {
                        client.Close();
                        return;
                    };
                    HandleClientIO(stream, host, port);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        bool ParseHostPort(string data, out string host, out int port)
        {
            host = null;
            port = 0;
            var lines = data.Split("\r\n", StringSplitOptions.RemoveEmptyEntries);
            if (lines.Length == 0)
            {
                return false;
            }
            var connectMatch = httpRegex.Match(lines[0]);
            if (!connectMatch.Success)
            {
                return false;
            }
            if (connectMatch.Groups["method"].Value != "CONNECT")
            {
                return false;
            }
            var url = connectMatch.Groups["url"].Value;
            var uri = new Uri("http://" + url);
            host = uri.Host;
            port = uri.Port;
            return true;
        }
        async Task SendMessage(NetworkStream stream, string data)
        {
            byte[] msg = Encoding.ASCII.GetBytes(data);
            await stream.WriteAsync(msg, 0, msg.Length);            
        }
        void HandleClientIO(NetworkStream stream, string host, int port)
        {
            TcpClient client = new TcpClient();
            if (!client.ConnectAsync(host, port).Wait(10000))
            {
                throw new Exception("Could not connect to host: " + host + ":" + port);
            }
            SendMessage(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\n\r\n").Wait();
            using (var targetStream = client.GetStream())
            {
                Task send = SendClientData(stream, targetStream);
                Task receive = ReceiveClientData(stream, targetStream);
                Parallel.Invoke(
                    async () => await send,
                    async () => await receive
                    );
                Task.WaitAll(send, receive);
            }
        }
        async Task SendClientData(NetworkStream stream, NetworkStream targetStream)
        {
            await Task.Yield();
            try
            {
                int r;
                byte[] sendBuffer = new byte[BufferSize];
                while ((r = await stream.ReadAsync(sendBuffer, 0, sendBuffer.Length)) != 0)
                {
                    var data = Encoding.ASCII.GetString(sendBuffer, 0, r);
                    //Console.WriteLine("Received: {0}", data);

                    await targetStream.WriteAsync(sendBuffer, 0, r);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Send Connection Failed: " + ex.Message);
            }
        }
        async Task ReceiveClientData(NetworkStream stream, NetworkStream targetStream)
        {
            await Task.Yield();
            try
            {
                byte[] receiveBuffer = new byte[BufferSize];
                int i;
                while ((i = await targetStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length)) != 0)
                {
                    //var data2 = Encoding.ASCII.GetString(receiveBuffer, 0, i);
                    //Console.WriteLine("Remote: {0}", data2);
                    await stream.WriteAsync(receiveBuffer, 0, i);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Receive Connection Failed: " + ex.Message);
            }
        }
    }
}

and simply using in main:

class Program
    {
        static void Main(string[] args)
        {
            var server = new ProxyServer();
            server.RunServerForEver().Wait();
        }
    }

I execute the proxy and test it in Chrome and Firefox, it works well for many https and http websites. But unfortunately some websites does not respond to initial SSL handshake and the proxy waits for response until timeouts.

For example 'https://google.com' works but https://github.com/ does not work.


Solution

  • It may be related to TLS version supported by the websites.

    Despite TLS 1.0 and TLS 1.1 are deprecated, google still has kept them enabled while github doesn't.

    Here you can check the TLS versions of the websites that your http tunnel server doesn't work well with them and compare them with working ones.