首页 >综合 > > 正文

c#构建具有用户认证与管理的socks5代理服务端

发布日期:2023-05-17 21:30:22 来源:博客园 分享

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 客户端与服务端握手
VERSIONMETHODS_COUNTMETHODS
1字节1字节1到255字节,长度zMETHODS_COUNT
0x050x030x00 0x01 0x02
VERSION SOCKS协议版本,目前固定0x05METHODS_COUNT 客户端支持的认证方法数量METHODS 客户端支持的认证方法,每个方法占用1个字节

METHODS列表(其他的认证方法可以自行上网了解)


(资料图片仅供参考)

0x00 不需要认证(常用)0x02 账号密码认证(常用)2.1 server -> client 无需认证,直接进入第3步,命令过程
VERSIONMETHOD
1字节1字节
0x050x00
2.2、server -> client 密码认证
VERSIONMETHOD
1字节1字节
0x050x02
2.2.1、client -> server 客户端发送账号密码
VERSIONUSERNAME_LENGTHUSERNAMEPASSWORD_LENGTHPASSWORD
1字节1字节1到255字节1字节1到255字节
0x010x010x0a0x010x0a
VERSION 认证子协商版本(与SOCKS协议版本的0x05无关系)USERNAME_LENGTH 用户名长度USERNAME 用户名字节数组,长度为USERNAME_LENGTHPASSWORD_LENGTH 密码长度PASSWORD 密码字节数组,长度为PASSWORD_LENGTH2.2.2、server -> client 返回认证结果
VERSIONSTATUS
1字节1字节
0x010x00
VERSION 认证子协商版本STATUS 认证结果,0x00认证成功,大于0x00认证失败3.1 client -> server 发送连接请求
VERSIONCOMMANDRSVADDRESS_TYPEDST.ADDRDST.PORT
1字节1字节1字节1字节1-255字节2字节
VERSION SOCKS协议版本,固定0x05COMMAND 命令0x01 CONNECT 连接上游服务器0x02 BIND 绑定,客户端会接收来自代理服务器的链接,著名的FTP被动模式0x03 UDP ASSOCIATE UDP中继RSV 保留字段ADDRESS_TYPE 目标服务器地址类型0x01 IP V4地址0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组0x04 IP V6地址DST.ADDR 目标服务器地址(如果COMMAND是0x03,即UDP模式,此处为客户端启动UDP发送消息的主机地址)DST.PORT 目标服务器端口(如果COMMAND是0x03,即UDP模式,此处为客户端启动UDP发送消息的端口)3.2 server -> client 服务端响应连接结果
VERSIONRESPONSERSVADDRESS_TYPEDST.ADDRDST.PORT
1字节1字节1字节1字节1-255字节2字节
VERSION SOCKS协议版本,固定0x05RESPONSE 响应命令,除0x00外,其它响应都应该直接断开连接0x00 代理服务器连接目标服务器成功0x01 代理服务器故障0x02 代理服务器规则集不允许连接0x03 网络无法访问0x04 目标服务器无法访问(主机名无效)0x05 连接目标服务器被拒绝0x06 TTL已过期0x07 不支持的命令0x08 不支持的目标服务器地址类型0x09 - 0xFF 未分配RSV 保留字段BND.ADDR 代理服务器连接目标服务器成功后的代理服务器IPBND.PORT 代理服务器连接目标服务器成功后的代理服务器端口4、数据转发

第3步成功后,进入数据转发阶段

CONNECT 则将client过来的数据原样转发到目标,接着再将目标回来的数据原样返回给clientBINDUDP ASSOCIATEudp转发的数据包收到客户端udp数据包后,解析出目标地址,数据,然后把数据发送过去收到服务端回来的udp数据后,根据相同格式,打包,然后发回客户端
RSVFRAGADDRESS_TYPEDST.ADDRDST.PORTDATA
2字节1字节1字节可变长2字节可变长
RSV 保留为FRAG 分片位ATYP 地址类型0x01 IP V4地址0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组0x04 IP V6地址DST.ADDR 目标地址DST.PORT 目标端口DATA 数据状态机控制每个连接状态

从协议中我们可以看出,一个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