Socks 协议是一种代理 (Proxy) 协议, 例如我们所熟知的 Shdowsocks 便是 Socks 协议的一个典型应用程序, Socks 协议有多个版本, 目前最新的版本为 5, 其协议标准文档为 RFC 1928。我们一起来使用.net 7 构建一个支持用户管理的高性能socks5代理服务端
目录协议流程1 client -> server 客户端与服务端握手2.1 server -> client 无需认证,直接进入第3步,命令过程2.2、server -> client 密码认证2.2.1、client -> server 客户端发送账号密码2.2.2、server -> client 返回认证结果3.1 client -> server 发送连接请求3.2 server -> client 服务端响应连接结果4、数据转发udp转发的数据包状态机控制每个连接状态连接与用户管理持久化效果示例源码以及如何使用协议流程1 client -> server 客户端与服务端握手VERSION | METHODS_COUNT | METHODS |
---|---|---|
1字节 | 1字节 | 1到255字节,长度zMETHODS_COUNT |
0x05 | 0x03 | 0x00 0x01 0x02 |
METHODS列表(其他的认证方法可以自行上网了解)
(资料图片仅供参考)
0x00 不需要认证(常用)0x02 账号密码认证(常用)2.1 server -> client 无需认证,直接进入第3步,命令过程VERSION | METHOD |
---|---|
1字节 | 1字节 |
0x05 | 0x00 |
VERSION | METHOD |
---|---|
1字节 | 1字节 |
0x05 | 0x02 |
VERSION | USERNAME_LENGTH | USERNAME | PASSWORD_LENGTH | PASSWORD |
---|---|---|---|---|
1字节 | 1字节 | 1到255字节 | 1字节 | 1到255字节 |
0x01 | 0x01 | 0x0a | 0x01 | 0x0a |
VERSION | STATUS |
---|---|
1字节 | 1字节 |
0x01 | 0x00 |
VERSION | COMMAND | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
VERSION | RESPONSE | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
第3步成功后,进入数据转发阶段
CONNECT 则将client过来的数据原样转发到目标,接着再将目标回来的数据原样返回给clientBINDUDP ASSOCIATEudp转发的数据包收到客户端udp数据包后,解析出目标地址,数据,然后把数据发送过去收到服务端回来的udp数据后,根据相同格式,打包,然后发回客户端RSV | FRAG | ADDRESS_TYPE | DST.ADDR | DST.PORT | DATA |
---|---|---|---|---|---|
2字节 | 1字节 | 1字节 | 可变长 | 2字节 | 可变长 |
从协议中我们可以看出,一个Socks5协议的连接需要经过握手,认证(可选),建立连接三个流程。那么这是典型的符合状态机模型的业务流程。
创建状态和事件枚举
public enum ClientState { Normal, ToBeCertified, Certified, Connected, Death } public enum ClientStateEvents { OnRevAuthenticationNegotiation, //当收到客户端认证协商 OnRevClientProfile, //收到客户端的认证信息 OnRevRequestProxy, //收到客户端的命令请求请求代理 OnException, OnDeath }
根据服务器是否配置需要用户名密码登录,从而建立正确的状态流程。
if (clientStatehandler.NeedAuth) { builder.In(ClientState.Normal) .On(ClientStateEvents.OnRevAuthenticationNegotiation) .Goto(ClientState.ToBeCertified) .Execute(clientStatehandler.HandleAuthenticationNegotiationRequestAsync) .On(ClientStateEvents.OnException) .Goto(ClientState.Death); } else { builder.In(ClientState.Normal) .On(ClientStateEvents.OnRevAuthenticationNegotiation) .Goto(ClientState.Certified) .Execute(clientStatehandler.HandleAuthenticationNegotiationRequestAsync) .On(ClientStateEvents.OnException) .Goto(ClientState.Death); } builder.In(ClientState.ToBeCertified) .On(ClientStateEvents.OnRevClientProfile) .Goto(ClientState.Certified) .Execute(clientStatehandler.HandleClientProfileAsync) .On(ClientStateEvents.OnException) .Goto(ClientState.Death); ; builder.In(ClientState.Certified) .On(ClientStateEvents.OnRevRequestProxy) .Goto(ClientState.Connected) .Execute(clientStatehandler.HandleRequestProxyAsync) .On(ClientStateEvents.OnException) .Goto(ClientState.Death); builder.In(ClientState.Connected).On(ClientStateEvents.OnException).Goto(ClientState.Death);
在状态扭转中如果出现异常,则直接跳转状态到“Death”,
_machine.TransitionExceptionThrown += async (obj, e) => { _logger.LogError(e.Exception.ToString()); await _machine.Fire(ClientStateEvents.OnException); };
对应状态扭转创建相应的处理方法, 基本都是解析客户端发来的数据包,判断是否合理,最后返回一个响应。
/// /// 处理认证协商 /// /// /// /// /// public async Task HandleAuthenticationNegotiationRequestAsync(UserToken token) { if (token.ClientData.Length < 3) { await token.ClientSocket.SendAsync(new byte[] { 0x05, _exceptionCode }); throw new ArgumentException("Error request format from client."); } if (token.ClientData.Span[0] != 0x05) //socks5默认头为5 { await token.ClientSocket.SendAsync(new byte[] { 0x05, _exceptionCode }); throw new ArgumentException("Error request format from client."); } int methodCount = token.ClientData.Span[1]; if (token.ClientData.Length < 2 + methodCount) //校验报文 { await token.ClientSocket.SendAsync(new byte[] { 0x05, _exceptionCode }); throw new ArgumentException("Error request format from client."); } bool supprtAuth = false; for (int i = 0; i < methodCount; i++) { if (token.ClientData.Span[2 + i] == 0x02) { supprtAuth = true; break; } } if (_serverConfiguration.NeedAuth && !supprtAuth) //是否支持账号密码认证 { await token.ClientSocket.SendAsync(new byte[] { 0x05, _exceptionCode }); throw new InvalidOperationException("Can"t support password authentication!"); } await token.ClientSocket.SendAsync(new byte[] { 0x05, (byte)(_serverConfiguration.NeedAuth ? 0x02 : 0x00) }); } /// /// 接收到客户端认证 /// /// /// public async Task HandleClientProfileAsync(UserToken token) { var version = token.ClientData.Span[0]; //if (version != _serverConfiguration.AuthVersion) //{ // await token.ClientSocket.SendAsync(new byte[] { 0x05, _exceptionCode }); // throw new ArgumentException("The certification version is inconsistent"); //} var userNameLength = token.ClientData.Span[1]; var passwordLength = token.ClientData.Span[2 + userNameLength]; if (token.ClientData.Length < 3 + userNameLength + passwordLength) { await token.ClientSocket.SendAsync(new byte[] { 0x05, _exceptionCode }); throw new ArgumentException("Error authentication format from client."); } var userName = Encoding.UTF8.GetString(token.ClientData.Span.Slice(2, userNameLength)); var password = Encoding.UTF8.GetString(token.ClientData.Span.Slice(3 + userNameLength, passwordLength)); var user = await _userService.FindSingleUserByUserNameAndPasswordAsync(userName, password); if (user == null || user.ExpireTime < DateTime.Now) { await token.ClientSocket.SendAsync(new byte[] { version, _exceptionCode }); throw new ArgumentException($"User{userName}尝试非法登录"); } token.UserName = user.UserName; token.Password = user.Password; token.ExpireTime = user.ExpireTime; await token.ClientSocket.SendAsync(new byte[] { version, 0x00 }); } /// /// 客户端请求连接 /// /// /// public async Task HandleRequestProxyAsync(UserToken token) { var data = token.ClientData.Slice(3); Socks5CommandType socks5CommandType = (Socks5CommandType)token.ClientData.Span[1]; var proxyInfo = _byteUtil.GetProxyInfo(data); var serverPort = BitConverter.GetBytes(_serverConfiguration.Port); if (socks5CommandType == Socks5CommandType.Connect) //tcp { //返回连接成功 IPEndPoint targetEP = new IPEndPoint(proxyInfo.Item2, proxyInfo.Item3);//目标服务器的终结点 token.ServerSocket = new Socket(targetEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); token.ServerSocket.Bind(new IPEndPoint(IPAddress.Any, 0)); var e = new SocketAsyncEventArgs { RemoteEndPoint = new IPEndPoint(targetEP.Address, targetEP.Port) }; token.ServerSocket.ConnectAsync(e); e.Completed += async (e, a) => { try { token.ServerBuffer = new byte[800 * 1024];//800kb token.StartTcpProxy(); var datas = new List { 0x05, 0x0, 0, (byte)Socks5AddressType.IPV4 }; foreach (var add in (token.ServerSocket.LocalEndPoint as IPEndPoint).Address.GetAddressBytes()) { datas.Add(add); } //代理端启动的端口信息回复给客户端 datas.AddRange(BitConverter.GetBytes((token.ServerSocket.LocalEndPoint as IPEndPoint).Port).Take(2).Reverse()); await token.ClientSocket.SendAsync(datas.ToArray()); } catch (Exception) { token.Dispose(); } }; } else if (socks5CommandType == Socks5CommandType.Udp)//udp { token.ClientUdpEndPoint = new IPEndPoint(proxyInfo.Item2, proxyInfo.Item3);//客户端发起代理的udp终结点 token.IsSupportUdp = true; token.ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); token.ServerSocket.Bind(new IPEndPoint(IPAddress.Any, 0)); token.ServerBuffer = new byte[800 * 1024];//800kb token.StartUdpProxy(_byteUtil); var addressBytes = (token.ServerSocket.LocalEndPoint as IPEndPoint).Address.GetAddressBytes(); var portBytes = BitConverter.GetBytes((token.ServerSocket.LocalEndPoint as IPEndPoint).Port).Take(2).Reverse().ToArray(); await token.ClientSocket.SendAsync(new byte[] { 0x05, 0x0, 0, (byte)Socks5AddressType.IPV4, addressBytes[0], addressBytes[1], addressBytes[2], addressBytes[3], portBytes[0], portBytes[1] }); } else { await token.ClientSocket.SendAsync(new byte[] { 0x05, 0x1, 0, (byte)Socks5AddressType.IPV4, 0, 0, 0, 0, 0, 0 }); throw new Exception("Unsupport proxy type."); } }
连接与用户管理当服务器采用需要认证的配置时,我们会返回给客户端0x02的认证方式,此时,客户端需要上传用户名和密码,如果认证成功我们就可以将用户信息与连接对象做绑定,方便后续管理。
在客户端通过tcp或者udp上传数据包,需要代理服务器转发时,我们记录数据包的大小作为上传数据包流量记录下来,反之亦然。示例:记录tcp代理客户端的下载流量
public void StartTcpProxy() { Task.Run(async () => { while (true) { var data = await ServerSocket.ReceiveAsync(ServerBuffer); if (data == 0) { Dispose(); } await ClientSocket.SendAsync(ServerBuffer.AsMemory(0, data)); if (!string.IsNullOrEmpty(UserName)) ExcuteAfterDownloadBytes?.Invoke(UserName, data); } }, CancellationTokenSource.Token); }
当管理界面修改某用户的密码或者过期时间的时候1.修改密码,强制目前所有使用该用户名密码的连接断开2.我们每个连接会有一个定时服务,判断是否过期从而实现用户下线。
//更新密码或者过期时间后public void UpdateUserPasswordAndExpireTime(string password, DateTime dateTime) { if (password != Password) { Dispose(); } if (DateTime.Now > ExpireTime) { Dispose(); } }/// /// 过期自动下线 /// public void WhenExpireAutoOffline() { Task.Run(async () => { while (true) { if (DateTime.Now > ExpireTime) { Dispose(); } await Task.Delay(1000); } }, CancellationTokenSource.Token); }
持久化用户数据包括,用户名密码,使用流量,过期时间等存储在server端的sqlite数据库中。通过EFcore来增删改查。如下定期更新用户流量到数据库
private void LoopUpdateUserFlowrate() { Task.Run(async () => { while (true) { var datas = _uploadBytes.Select(x => { return new { UserName = x.Key, AddUploadBytes = x.Value, AddDownloadBytes = _downloadBytes.ContainsKey(x.Key) ? _downloadBytes[x.Key] : 0 }; }); if (datas.Count() <= 0 || (datas.All(x => x.AddUploadBytes == 0) && datas.All(x => x.AddDownloadBytes == 0))) { await Task.Delay(5000); continue; } var users = await _userService.Value.GetUsersInNamesAsync(datas.Select(x => x.UserName)); foreach (var item in datas) { users.FirstOrDefault(x => x.UserName == item.UserName).UploadBytes += item.AddUploadBytes; users.FirstOrDefault(x => x.UserName == item.UserName).DownloadBytes += item.AddDownloadBytes; } await _userService.Value.BatchUpdateUserAsync(users); _uploadBytes.Clear(); _downloadBytes.Clear(); await Task.Delay(5000); } }); }//批量更新用户信息到sqlite public async Task BatchUpdateUserFlowrateAsync(IEnumerable users) { using (var context = _dbContextFactory.CreateDbContext()) { context.Users.UpdateRange(users); await context.SaveChangesAsync(); } }
效果示例打开服务
打开Proxifier配置到我们的服务
查看Proxifier已经流量走到我们的服务
服务端管理器
源码以及如何使用https://github.com/BruceQiu1996/Socks5Server
标签:
Copyright © 2015-2022 时代兽药网版权所有 备案号: 联系邮箱: 514 676 113@qq.com