Loading

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

Socks 协议是一种代理 (Proxy) 协议, 例如我们所熟知的 Shdowsocks 便是 Socks 协议的一个典型应用程序, Socks 协议有多个版本, 目前最新的版本为 5, 其协议标准文档为 RFC 1928。
我们一起来使用.net 7 构建一个支持用户管理的高性能socks5代理服务端

协议流程

1 client -> server 客户端与服务端握手
VERSION METHODS_COUNT METHODS
1字节 1字节 1到255字节,长度zMETHODS_COUNT
0x05 0x03 0x00 0x01
0x02
  1. VERSION SOCKS协议版本,目前固定0x05
  2. METHODS_COUNT 客户端支持的认证方法数量
  3. METHODS 客户端支持的认证方法,每个方法占用1个字节

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

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

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

  1. CONNECT 则将client过来的数据原样转发到目标,接着再将目标回来的数据原样返回给client
  2. BIND
  3. UDP ASSOCIATE
udp转发的数据包
  1. 收到客户端udp数据包后,解析出目标地址,数据,然后把数据发送过去
  2. 收到服务端回来的udp数据后,根据相同格式,打包,然后发回客户端
RSV FRAG ADDRESS_TYPE DST.ADDR DST.PORT DATA
2字节 1字节 1字节 可变长 2字节 可变长
  1. RSV 保留为
  2. FRAG 分片位
  3. ATYP 地址类型
    1. 0x01 IP V4地址
    2. 0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组
    3. 0x04 IP V6地址
  4. DST.ADDR 目标地址
  5. DST.PORT 目标端口
  6. 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<UserToken>(clientStatehandler.HandleAuthenticationNegotiationRequestAsync)
                    .On(ClientStateEvents.OnException)
                    .Goto(ClientState.Death);
            }
            else 
            {
                builder.In(ClientState.Normal)
                        .On(ClientStateEvents.OnRevAuthenticationNegotiation)
                        .Goto(ClientState.Certified)
                        .Execute<UserToken>(clientStatehandler.HandleAuthenticationNegotiationRequestAsync)
                        .On(ClientStateEvents.OnException)
                        .Goto(ClientState.Death);
            }

            builder.In(ClientState.ToBeCertified)
                .On(ClientStateEvents.OnRevClientProfile)
                .Goto(ClientState.Certified)
                .Execute<UserToken>(clientStatehandler.HandleClientProfileAsync)
                .On(ClientStateEvents.OnException)
                .Goto(ClientState.Death); ;

            builder.In(ClientState.Certified)
                .On(ClientStateEvents.OnRevRequestProxy)
                .Goto(ClientState.Connected)
                .Execute<UserToken>(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);
            };

对应状态扭转创建相应的处理方法, 基本都是解析客户端发来的数据包,判断是否合理,最后返回一个响应。

/// <summary>
        /// 处理认证协商
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        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) });
        }

        /// <summary>
        /// 接收到客户端认证
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        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 });
        }

        /// <summary>
        /// 客户端请求连接
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        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<byte> { 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();
            }
        }

/// <summary>
        /// 过期自动下线
        /// </summary>
        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<User> users)
        {
            using (var context = _dbContextFactory.CreateDbContext())
            {
                context.Users.UpdateRange(users);
                await context.SaveChangesAsync();
            }
        }

效果示例

打开服务
image

打开Proxifier配置到我们的服务
image

查看Proxifier已经流量走到我们的服务
image

服务端管理器
image

源码以及如何使用

https://github.com/BruceQiu1996/Socks5Server

posted @ 2023-05-17 21:13  BruceNeter  阅读(1368)  评论(0编辑  收藏  举报