本文将演示一种SOCKET代理的实现,利用这种代理,可以接入普通的TCP通信,在代理的同时还可以提供附加私有认证、加密和通信控制功能。演示所用的场景是WOW客户端登录流程,并且只实现了加密功能,额外认证功能没有做。

一、WOW登录流程

0.0 本文不讨论任何与WOWSF搭建的有关信息,请支持正版游戏。

0.1 抵制不良游戏,拒绝盗版游戏;注意自我保护,谨防受骗上当;适度游戏益脑,沉迷游戏伤身;合理安排时间,享受健康生活。

0.2 WOW的服务器有两种类型

  • 登录服务器,用于账号登录、账号管理、真实游戏服务器列表相关功能,一般使用TCP 3724端口提供服务;
  • 游戏服务器,进入游戏以后,客户端通信的对象,端口一般是TCP 8085。

1. 输入账号密码后,客户端首先联系登录服务器;

2. 一旦登录服务器验证通过,玩家就可以进行服务器选择、角色查看与管理,此时客户端也会获取到真实游戏服务器的地址备用(本文将演示如何通过代理接管步骤1和2中的登录服务器通信,篇幅有限,不对游戏服务器进行代理);

3. 玩家选择角色开始游戏时,客户端连接真实游戏服务器,开始与游戏服务器的通信过程。

二、代理工作流程

1. 代理如何接管会话

抛开本文的目的直接看一个TCP通信,无非就是A向B发起连接、B接收以后建立连接、然后数据始终在A和B建立起的这个连接中进行传输(TCP特性之一:面向连接)。

代理就工作在AB之间,作为中间人存在:A向代理请求连接,代理向B请求连接,一切就绪后,事实上通信被切成A<->Proxy和Proxy<->B两部分,就好像在一条电缆中间切开,加入一个双刀开关。

而Proxy最基本的工作内容——代理,实现起来只有两个部分:将A发的内容转给B,将B发的内容转给A(充当传话人的角色)。

2. 认证、加密和连接控制

上图演示的结构中,A和B的通信协议由AB双方约定,与Proxy完全无关,Proxy角色只是将所有数据原封不动地转发到目的地,根本无从接管。控制措施意味着要在原始数据上附加控制相关的内容,但发向A和B的数据又不能修改,否则就破坏了AB的会话,破坏了正常通信。所以要在保证正常通信的前提下加入控制措施,必须由成对的代理来实现。

上图是一种非常简陋的结构演示,ProxyA和ProxyB在互相传输数据的过程中,发送数据的时候进行加密,接收到数据时进行解密,这样既不破坏A和B之间的通信,也额外地加入了数据控制(有些网银的支付页面会显示成127.0.0.1,就是使用这种模式)。

如果想实现认证类的协议,或者希望实现增强型的数据加密,需要预先定义一下数据结构,将AB的原始数据标记成TypeA,将认证数据标记成TypeB,将密钥交换数据标记成TypeC……将实现功能所需要的每种数据都进行标记以便区分处理。

本文只演示最简单的加密过程,如果有更深入的加密、认证需求(账户、密码登录功能,账户相关的权限功能,增强的加密措施如RSA,流量/时长计费功能等等),可以在此基础上进行扩充。

3. 反向代理

反向代理与本文介绍的内容无关,但它的基础架构与之类似,所以跑题提一下。

反向代理并非只能部署在网站相同服务器上,甚至不需要部署在网站相同内网里而是跨公网代理,唯一的要求就是:反向代理到网站服务器可达。最终实现隐藏网站、在客户端到网站之间不可达的场景里借由反向代理实现访问、对多个网站进行聚合的目的(没有服务注册的简陋版微服务)。

3.1 场景一:

网站以HTTP发布在TCP8080端口,反向代理创建一个vhost,发布到ssl 443并绑定证书,通过proxy_pass将网站代理到根路径 /,这样就完成了一个https发布内网网站的反向代理架设(如果只有这个配置,网站应用无法获取到客户端真实IP,所以一般还会增加X-Forwarded-For和X-Real-IP配置项,实现客户端的真实地址获取以及溯源)。后端的真实网站应用可以扩展成多个,实现负载均衡的目的。而部署SSL证书,则可以视为前文提到的增强加密手段。

3.2 场景二:

发布的网站使用了Google Fonts,但对应服务的域名被屏蔽(现在在大陆已经可以直接访问字体了),为了解决这个问题,在可以访问字体服务的服务器上架设一台反向代理,网站中的字体资源链接全部替换成反向代理服务器的链接,实现正常访问。

* 不要用这个方法爬梯,没用。

3. 本文案例中的代理

3.1 客户端的登录地址修改为localhost,而非真实的登录服务器,客户端登录时只和ProxyClient通信;

3.2 ProxyClient部署在客户端本地,监听localhost TCP 3724(如果监听IP改为物理网卡IP,这个ProxyClient就可以接受来自其它PC的登录请求,直接变成一台登录服务的前置服务器);

3.3 ProxyClient收到客户端发起的连接请求时,连接到部署在服务器上的ProxyServer预定端口(本文使用13724);

3.4 ProxyServer收到来自ProxyClient的连接请求时,连接到localhost的3724(本机真实服务端的端口),自此,一条Client <-> ProxyClient <-> ProxyServer <-> Server的链接就打通了;

3.5 无论数据是从Client发到Server,还是反方向传输,都由本侧代理接收,加密发到对侧代理,对侧代理收到数据后,解密发到真实目的地;

3.6 上图只是一种相对比较便利的部署结构演示,实际两个代理的位置并没有硬性要求,如果有其它特殊需求,完全可以换一种方式部署。

三、ProxyClient


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;

namespace ProxyClient
{
    class SocketState
    {
        public static int BufferLength = 2048;
        //以Listener的远端端口作为字典索引
        //(即真实客户端的本地端口,发生重复代表原客户端已离线)
        // 选择依据:不可能产生重复或产生重复也无所谓的一个数值
        public int Index { get; set; }
        public Socket Socket { get; set; }
        public byte[] Buffer { get; set; } = new byte[BufferLength];
    }
    class Program
    {
        // 以本地端口号为索引,储存两个SocketState(Socket+Buffer)
        // Listener用于本地监听,Sender用于远程服务器通信
        static Dictionary<int, (SocketState Listener, SocketState Sender)> States =
            new Dictionary<int, (SocketState Listener, SocketState Sender)>();

        static void Main(string[] args)
        {
            //在本地TCP 3724侦听,接收客户端连接,这个端口与
            //realmlist.wtf文件中的端口需要一致
            Socket listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
            listener.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3724));
            listener.Listen(50);
            listener.BeginAccept(new AsyncCallback(Connect), listener);
            WriteLog("Client Agent Started.");
            while (true) Console.ReadLine();
        }
        static void Connect(IAsyncResult ar)
        {
            WriteLog("New Client Connected.");
            //stateSender用于保存与服务器侧代理通信的Socket和Buffer
            SocketState stateSender = new SocketState()
            {
                Socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
            };

            Socket socketListener = ar.AsyncState as Socket;
            //stateListener专门用于保存本地客户端通信的Socket和对应Buffer
            SocketState stateListener = new SocketState()
            {
                Socket = socketListener.EndAccept(ar)
            };
            socketListener.BeginAccept(new AsyncCallback(Connect), socketListener);

            stateListener.Index = (stateListener.Socket.RemoteEndPoint as IPEndPoint).Port;
            stateSender.Index = stateListener.Index;

            //用真实客户端本地端口为索引,保存两个State,方便在回调中引用
            States[stateListener.Index] = (stateListener, stateSender);

            //当有客户端连接过来时,向服务器侧的代理发起连接,并开始接收数据
            stateSender.Socket.Connect(IPAddress.Parse("192.168.2.50"), 13724);
            stateSender.Socket.BeginReceive(
                stateSender.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None,
                new AsyncCallback(SenderReceive),
                stateSender);

            stateListener.Socket.BeginReceive(
                stateListener.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None,
                new AsyncCallback(ListenerReceive),
                stateListener);
        }
        static void ListenerReceive(IAsyncResult ar)
        {
            SocketState state = ar.AsyncState as SocketState;
            int length = state.Socket.EndReceive(ar);
            if (length > 0)
            {
                WriteLog($"Client ->: {length}");

                // 从客户端收到的数据加密发给服务器侧的代理
                SocketState sender = States[state.Index].Sender;
                sender.Socket.Send(Encode(state.Buffer, length));
            }

            state.Socket.BeginReceive(
                state.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None,
                new AsyncCallback(ListenerReceive),
                state);
        }

        static void SenderReceive(IAsyncResult ar)
        {
            SocketState state = ar.AsyncState as SocketState;
            int length = state.Socket.EndReceive(ar);
            if (length > 0)
            {
                WriteLog($"<- Server: {length}");

                // 从服务器收到的数据解密发给客户端
                SocketState listener = States[state.Index].Listener;
                listener.Socket.Send(Encode(state.Buffer, length));
            }

            state.Socket.BeginReceive(
                state.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None, new AsyncCallback(SenderReceive), state);
        }
        static void WriteLog(string msg)
        {
            Console.WriteLine(
                DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss\t") + msg);
        }
        /// <summary>
        /// 用按位取反来模拟加密算法
        /// </summary>
        /// <param name="data"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        static byte[] Encode(byte[] data, int length) =>
            data.Take(length).Select(b => (byte)~b).ToArray();
    }
}

四、ProxyServer

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;

namespace ProxyServer
{
    class SocketState
    {
        public static int BufferLength = 2048;
        //以Listener的对端IP+端口作为字典索引
        //(即客户端侧代理程序)
        // 选择依据:不可能产生重复或重复也无所谓的值
        public string Index { get; set; }
        public Socket Socket { get; set; }
        public byte[] Buffer { get; set; } = new byte[BufferLength];
    }
    class Program
    {
        // 以Listener的对端IP+端口为索引,储存两个SocketState(Socket+Buffer)
        // Listener用于对外服务,Sender用于连接本地真实服务器
        static Dictionary<string, (SocketState Listener, SocketState Sender)> States =
            new Dictionary<string, (SocketState Listener, SocketState Sender)>();

        static void Main(string[] args)
        {
            //在本地13724侦听,接收来自客户端的代理连接
            Socket listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
            listener.Bind(new IPEndPoint(IPAddress.Parse("192.168.2.50"), 13724));
            listener.Listen(50);
            listener.BeginAccept(new AsyncCallback(Connect), listener);
            WriteLog("Server Agent Started.");
            while (true) Console.ReadLine();
        }
        static void Connect(IAsyncResult ar)
        {
            WriteLog("New Client Connected.");
            //stateSender专门用于保存与真实服务器通信的Socket和Buffer
            SocketState stateSender = new SocketState()
            {
                Socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
            };

            Socket socketListener = ar.AsyncState as Socket;
            //stateListener专门用于保存与客户端侧代理通信的Socket和Buffer
            SocketState stateListener = new SocketState()
            {
                Socket = socketListener.EndAccept(ar)
            };
            socketListener.BeginAccept(new AsyncCallback(Connect), socketListener);

            //当有客户端连接时,向本地真实服务器发起连接,并开始接收数据
            stateSender.Socket.Connect(IPAddress.Parse("127.0.0.1"), 3724);

            //用Sender的本地端口为索引,保存两个State,方便在回调中引用
            stateSender.Index = stateListener.Socket.RemoteEndPoint.ToString(); //as IPEndPoint).ToString();
            stateListener.Index = stateSender.Index;
            States[stateListener.Index] = (stateListener, stateSender);

            stateSender.Socket.BeginReceive(
                stateSender.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None,
                new AsyncCallback(SenderReceive),
                stateSender);

            stateListener.Socket.BeginReceive(
                stateListener.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None,
                new AsyncCallback(ListenerReceive),
                stateListener);
        }
        static void ListenerReceive(IAsyncResult ar)
        {
            SocketState state = ar.AsyncState as SocketState;
            int length = state.Socket.EndReceive(ar);
            if (length > 0)
            {
                WriteLog($"Client ->: {length}");

                // 从ProxyClient收到数据后解密发给服务器
                SocketState sender = States[state.Index].Sender;
                sender.Socket.Send(Encode(state.Buffer, length));
            }

            state.Socket.BeginReceive(
                state.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None,
                new AsyncCallback(ListenerReceive),
                state);
        }

        static void SenderReceive(IAsyncResult ar)
        {
            SocketState state = ar.AsyncState as SocketState;
            int length = state.Socket.EndReceive(ar);
            if (length > 0)
            {
                WriteLog($"<- Server: {length}");

                // 从服务器收到的数据解密发给ProxyClient
                SocketState listener = States[state.Index].Listener;
                listener.Socket.Send(Encode(state.Buffer, length));
            }

            state.Socket.BeginReceive(
                state.Buffer,
                0,
                SocketState.BufferLength,
                SocketFlags.None, new AsyncCallback(SenderReceive), state);
        }
        static void WriteLog(string msg)
        {
            Console.WriteLine(
                DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss\t") + msg);
        }
        /// <summary>
        /// 用按位取反来模拟加密算法,实际应用时以真实加密算法替代,
        /// 同时也需要准备一套相应的解密算法联合使用。
        /// </summary>
        /// <param name="data"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        static byte[] Encode(byte[] data, int length) =>
            data.Take(length).Select(b => (byte)~b).ToArray();
    }
}

五、效果演示

1. 添加两个账号,Proxy1和Proxy2

2. 修改登录服务器为127.0.0.1,并启动ProxyClient和ProxyServer

3. 登录两个账号,在账号上建立角色,并登录游戏(本文的代理只接管了登录服务器)

3.1 游戏画面

3.2 ProxyClient记录

3.3 ProxyServer记录

3.4 查看客户端电脑的连接情况

3.5 查看服务端的连接情况

分类: articles