C# Socket 使用 proxy 連線

合作的 partner 在資料介接上提供 socket 的接口來確保資料更新的即時性,但為了有基本安全性所以只允許 whitelist server 可以連線,這在 production server 是很常見的限制,甚至在互相允許的測試環境也是合理的,但在開發階段這樣的安全性要求就顯得有些窒礙難行,所以打算透過 proxy server 來連線做開發,連同測試環境也透過 proxy 來跟 partner 做溝通

因為過去我沒有 socket 相關經驗,所以花了點時間做了簡單的 socket client/server 來協助測試開發,不過也擔心自己寫的程式無法反應出實際狀況,姑且走一步算一步,先求有再求好囉

最近團隊的 proxy 多數都使用 goproxy,今天也會以 goproxy 做範例,對於 goproxy 的詳細使用說明也可以參考之前筆記:使用 goproxy

基本環境說明

  1. macOS Big Sur 11.5.1
  2. .NET Core SDK 5.0.202
  3. docker images

    • snail007/goproxy:v11.0
  4. NuGet packages

    • ProxySocket 1.1.2
  5. socket client/server

    詳細內容可以參考 yowko/SocketProxyDemo

    • server Program.cs

      using System;
      using System.Net;
      using System.Net.Sockets;
      using System.Text;
      using System.Threading;
              
      namespace SocketServer
      {
          public class ThreadWork
          {
              public static void Send(object obj)
              {
                  var client = ((Socket)obj);
                  while (true)
                  {
                      var input = Console.ReadLine();
                      if (input == "exit")
                      {
                          client.Close();
                          Environment.Exit(0);
                      }
              
                      Console.WriteLine("You: " + input);
              
                      client.Send(Encoding.UTF8.GetBytes(input));
                  }
              }
              
              public static void Receive(object obj)
              {
                  var client = ((Socket)obj);
                  while (true)
                  {
                      var data = new byte[1024];
              
                      var receiveData = client.Receive(data);
              
                      if (receiveData == 0)
                      {
                          Console.WriteLine("Disconnected from {0}", client.RemoteEndPoint);
              
                          break;
                      }
              
              
                      Console.WriteLine("Client: " + Encoding.UTF8.GetString(data, 0,         receiveData));
                  }
              
                  Environment.Exit(0);
              }
          }
              
          internal class Program
          {
              private static void Main(string[] args)
              {
                  Console.OutputEncoding = Encoding.UTF8;
              
                  var ipEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);
              
                  var newSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,         ProtocolType.Tcp);
              
                  newSocket.Bind(ipEndPoint);
              
                  newSocket.Listen(10);
              
              
                  AcceptClient(newSocket);
              }
              
              private static void AcceptClient(Socket newSocket)
              {
                  Console.WriteLine("Waiting for a client...");
              
                  var client = newSocket.Accept();
              
                  var clientEndpoint = (IPEndPoint)client.RemoteEndPoint;
              
                  Console.WriteLine("Connected with {0} at port {1}", clientEndpoint.        Address, clientEndpoint.Port);
              
                  const string welcome = "Welcome to my test server";
              
                  var data = Encoding.UTF8.GetBytes(welcome);
              
                  client.Send(data, data.Length, SocketFlags.None);
              
              
                  var sendThread = new Thread(ThreadWork.Send);
                  var receiveThread = new Thread(ThreadWork.Receive);
              
                  sendThread.Start(client);
                  receiveThread.Start(client);
              }
          }
      }
      
    • client Program.cs

      using System;
      using System.Net;
      using System.Net.Sockets;
      using System.Text;
      using System.Threading;
              
      namespace SocketClient
      {
          public class ThreadWork
          {
              public static void Send(object obj)
              {
                  while (true)
                  {
                      var input = Console.ReadLine();
                      var server = ((Socket)obj);
              
                      if (input == "exit")
                      {
                          Console.WriteLine("Disconnecting from server...");
              
                          server.Shutdown(SocketShutdown.Both);
              
                          server.Close();
              
                          Console.WriteLine("Disconnected!");
              
                          Console.ReadLine();
                          Environment.Exit(0);
                      }
              
              
                      Console.WriteLine("You: " + input);
              
                      server.Send(Encoding.UTF8.GetBytes(input));
                  }
              }
              
              public static void Receive(object server)
              {
                  while (true)
                  {
                      var data = new byte[1024];
              
                      var receiveData = ((Socket)server).Receive(data);
              
                      var stringData = Encoding.UTF8.GetString(data, 0, receiveData);
              
                      Console.WriteLine("Server: " + stringData);
                  }
              }
          }
              
          internal class Program
          {
              private static void Main(string[] args)
              {
                  Console.OutputEncoding = Encoding.UTF8;
              
                  var data = new byte[1024];
                  var server = new Socket(AddressFamily.InterNetwork, SocketType.Stream,         ProtocolType.Tcp);
              
                  var ipEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);
                          
                  try
                  {
                      server.Connect(ipEndpoint);
                  }
                  catch (SocketException e)
                  {
                      Console.WriteLine("Unable to connect to server.");
              
                      Console.WriteLine(e.ToString());
              
                      return;
                  }
              
                  var receiveData = server.Receive(data);
              
                  var stringData = Encoding.UTF8.GetString(data, 0, receiveData);
              
                  Console.WriteLine(stringData);
              
                  var sendThread = new Thread(ThreadWork.Send);
                  sendThread.Start(server);
              
                  var receiveThread = new Thread(ThreadWork.Receive);
                  receiveThread.Start(server);
              }
          }
      }
      

使用方式

  • client 安裝 NuGet 套件: ProxySocket

    dotnet add package ProxySocket --version 1.1.2
    
  1. 使用 http proxy

    • proxy 設定

      docker run --rm -d -p 33080:33080 snail007/goproxy http -p :33080
      
    • 程式調整

      //對於 server 的 socket 改用 ProxySocket 來建立
      var server = new ProxySocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      //指定 proxy 的 endpoint
      var proxy = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 33080);
      //將上述的 proxy endpoint 設定給 ProxySocket
      server.ProxyEndPoint = proxy;
      //指定 ProxySocket 使用的 proxy 類型
      server.ProxyType = ProxyTypes.Https;
      

      3httpcode

    • 實際效果:正常連線

      • client

        1httpclient

      • server

        2httpserver

      • proxy

        11httpproxy

  2. 使用 socks5 proxy

    • proxy 設定

      docker run --rm -d -p 33081:33080 snail007/goproxy socks -p :33080
      
    • 程式調整

      //對於 server 的 socket 改用 ProxySocket 來建立
      var server = new ProxySocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      //指定 proxy 的 endpoint
      var proxy = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 33081);
      //將上述的 proxy endpoint 設定給 ProxySocket
      server.ProxyEndPoint = proxy;
      //指定 ProxySocket 使用的 proxy 類型
      server.ProxyType = ProxyTypes.Socks5;
      

      4socks5code

    • 實際效果:正常連線

      • client

        5socks5client

      • server

        6socks5server

      • proxy

        10socksproxy

  3. 不使用 proxy

    • 程式調整

      //對於 server 的 socket 改用 ProxySocket 來建立
      var server = new ProxySocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      //指定 proxy 的 endpoint
      var proxy = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 33081);
      //將上述的 proxy endpoint 設定給 ProxySocket
      server.ProxyEndPoint = proxy;
      //指定 ProxySocket 使用的 proxy 類型
      server.ProxyType = ProxyTypes.None;
      

      7nonecode

    • 實際效果:正常連線

      • client

        8noneclient

      • server

        9noneserver

心得

poma/ProxySocket 是個非常好用的套件,個人覺得可以將 socket 連線都換成這個套件,如果不需要 proxy 時就將 ProxyType 設定成 ProxyTypes.None 即可停用 proxy 了

另外需要特別留意的是我用了簡單的方式建立 socket client server,可能沒辦法完整模擬實際情境,在實際使用上還需要另外測試

詳細程式碼跟歷程,請參考 yowko/SocketProxyDemo

參考資訊

  1. 使用 goproxy
  2. yowko/SocketProxyDemo
  3. poma/ProxySocket