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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章