本文将演示一种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 查看服务端的连接情况