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 |
- VERSION SOCKS协议版本,目前固定0x05
- METHODS_COUNT 客户端支持的认证方法数量
- METHODS 客户端支持的认证方法,每个方法占用1个字节
METHODS列表(其他的认证方法可以自行上网了解)
- 0x00 不需要认证(常用)
- 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 |
- VERSION 认证子协商版本(与SOCKS协议版本的0x05无关系)
- USERNAME_LENGTH 用户名长度
- USERNAME 用户名字节数组,长度为USERNAME_LENGTH
- PASSWORD_LENGTH 密码长度
- PASSWORD 密码字节数组,长度为PASSWORD_LENGTH
2.2.2、server -> client 返回认证结果
| VERSION | STATUS |
|---|---|
| 1字节 | 1字节 |
| 0x01 | 0x00 |
- VERSION 认证子协商版本
- STATUS 认证结果,0x00认证成功,大于0x00认证失败
3.1 client -> server 发送连接请求
| VERSION | COMMAND | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
|---|---|---|---|---|---|
| 1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
- VERSION SOCKS协议版本,固定0x05
- COMMAND 命令
- 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 服务端响应连接结果
| VERSION | RESPONSE | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
|---|---|---|---|---|---|
| 1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
- VERSION SOCKS协议版本,固定0x05
- RESPONSE 响应命令,除0x00外,其它响应都应该直接断开连接
- 0x00 代理服务器连接目标服务器成功
- 0x01 代理服务器故障
- 0x02 代理服务器规则集不允许连接
- 0x03 网络无法访问
- 0x04 目标服务器无法访问(主机名无效)
- 0x05 连接目标服务器被拒绝
- 0x06 TTL已过期
- 0x07 不支持的命令
- 0x08 不支持的目标服务器地址类型
- 0x09 - 0xFF 未分配
- RSV 保留字段
- BND.ADDR 代理服务器连接目标服务器成功后的代理服务器IP
- BND.PORT 代理服务器连接目标服务器成功后的代理服务器端口
4、数据转发
第3步成功后,进入数据转发阶段
- CONNECT 则将client过来的数据原样转发到目标,接着再将目标回来的数据原样返回给client
- BIND
- UDP ASSOCIATE
udp转发的数据包
- 收到客户端udp数据包后,解析出目标地址,数据,然后把数据发送过去
- 收到服务端回来的udp数据后,根据相同格式,打包,然后发回客户端
| RSV | FRAG | ADDRESS_TYPE | DST.ADDR | DST.PORT | DATA |
|---|---|---|---|---|---|
| 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<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();
}
}
效果示例
打开服务

打开Proxifier配置到我们的服务

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

服务端管理器

源码以及如何使用
https://github.com/BruceQiu1996/Socks5Server
c#构建具有用户认证与管理的socks5代理服务端的更多相关文章
- Mysql之用户认证授权管理
概述 Mysql的认证采用账号密码方式,其中账号由两个部分组成:Host和User:Host为允许登录的客户端Ip,User为当前登录的用户名. 授权没有采用典型的RBAC(基于角色的访问控制),而是 ...
- 基于 Egg.js 框架的 Node.js 服务构建之用户管理设计
前言 近来公司需要构建一套 EMM(Enterprise Mobility Management)的管理平台,就这种面向企业的应用管理本身需要考虑的需求是十分复杂的,技术层面管理端和服务端构建是架构核 ...
- Nodejs之MEAN栈开发(八)---- 用户认证与会话管理详解
用户认证与会话管理基本上是每个网站必备的一个功能.在Asp.net下做的比较多,大体的思路都是先根据用户提供的用户名和密码到数据库找到用户信息,然后校验,校验成功之后记住用户的姓名和相关信息,这个信息 ...
- flask设置cookie,设置session,模拟用户认证、模拟管理后台admin、模拟用户logout
设置cookie HTTP协议是无状态的,在一次请求响应结束后,服务器不会留下关于客户端状态的信息.但是对于某些web程序来说,客户端的信息有必要被记住,比如用户的登录状态,这样就可以根据用户的状态来 ...
- mongodb之用户/认证/角色/权限管理
前言 用户权限管理很重要,只给需要的权限,防止应用系统漏洞导致脱库 认证和授权 Authentication 认证识别,解决我是谁 Authorization 操作授权,我能做什么 认证机制 MONG ...
- Laravel 6 – 搭建管理后台的用户认证“脚手架”工具
1. 下载Laravel/ui 命令: composer require laravel/ui "^1.0" -dev 注意laravel framework 6只支持版本1的la ...
- Cassandra--启用用户认证和用户管理
======================================================== 启用用户认证和创建超级用户 需要针对每个节点进行配置修改和重启,但授权操作仅需要在任一 ...
- apache用户认证访问机制(转)
Apache服务器已经内置用户验证机制,大家只要适当的加以设置,便可以控制网站的某些部分要用户验证.前期准备,必须已经安装apache,如果还没安装,或者对安装很模糊的话,第1步:我们在/var/ww ...
- laravel 的用户认证
1.简介 Laravel 中实现用户认证非常简单.实际上,几乎所有东西都已经为你配置好了.配置文件位于config/auth.php,其中包含了用于调整认证服务行为的.文档友好的选项配置. 在底层代码 ...
- 基于Ajax与用户认证系统的登录验证
一.登录页面 from django.contrib import admin from django.urls import path from blog import views urlpatte ...
随机推荐
- python内置函数open()
open()函数 介绍 open()函数用于打开文件并创建文件对象. open()函数的语法格式: file = open(filename, mode='r', buffering=-1, enco ...
- ASP.NET Core Web API Swagger 按标签Tags分组排序显示
需求 swagger页面按标签Tags分组显示. 没有打标签Tags的接口,默认归到"未分组". 分组内按接口路径排序 说明 为什么没有使用GroupName对接口进行分组? 暂时 ...
- 使用float进行比较问题处理
float compare Abstract 使用float数据进行精确计算和比较,可能由于精度问题导致程序逻辑异常. Explanation 使用float数据进行比较,计算机表达double和fl ...
- 【Jenkins】linux与windows环境下的安装步骤
linux环境: <1>安装jdk: https://www.cnblogs.com/poloyy/p/12801792.htmljdk (参考) <2>环境清理 find / ...
- java多线程--6 死锁问题 锁Lock
java多线程--6 死锁问题 锁Lock 死锁问题 多个线程互相抱着对方需要的资源,然后形成僵持 死锁状态 package com.ssl.demo05; public class DeadLock ...
- vue核心原理(Diff算法、虚拟dom)
核心原理&源码 Diff 算法 这里参考大佬文章:https://mp.weixin.qq.com/s/oAlVmZ4Hbt2VhOwFEkNEhw diff 算法的进化 关于 diff 算法 ...
- 传输层和网络层的checksum区别,TCP cksum为何包含伪首部
一直搞不清传输层和网络层的校验和为什么校验内容不一样,最近问了一些前辈,找寻了一些答案,总结一下自己的思考. 先说一下传输层(TCP)和网络层(IP)的校验和: TCP校验和有伪首部.TCP herd ...
- 这可能是最全面的TCP面试八股文了
计算机网络基础,考验一个程序员的基本功,也能更快的筛选出更优秀的人才. 说说TCP的三次握手 假设发送端为客户端,接收端为服务端.开始时客户端和服务端的状态都是CLOSED. 第一次握手:客户端向服务 ...
- 云上大数据存储:探究 JuiceFS 与 HDFS 的异同
HDFS 作为 Hadoop 提供存储组件,已经成为大数据生态里面数据存储最常用的选择,通常在机房环境部署. JuiceFS 是一个基于对象存储的分布式文件系统,用户可以在云上快速地搭建按需扩容的弹性 ...
- 【ACM算法竞赛日常训练】DAY10题解与分析【月月给华华出题】【华华给月月出题】| 筛法 | 欧拉函数 | 数论
DAY10共2题: 月月给华华出题 华华给月月出题 难度较大. 作者:Eriktse 简介:211计算机在读,现役ACM银牌选手力争以通俗易懂的方式讲解算法!️欢迎关注我,一起交流C++/Python ...