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();
}
}

效果示例

打开服务

打开Proxifier配置到我们的服务

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

服务端管理器

源码以及如何使用

https://github.com/BruceQiu1996/Socks5Server

c#构建具有用户认证与管理的socks5代理服务端的更多相关文章

  1. Mysql之用户认证授权管理

    概述 Mysql的认证采用账号密码方式,其中账号由两个部分组成:Host和User:Host为允许登录的客户端Ip,User为当前登录的用户名. 授权没有采用典型的RBAC(基于角色的访问控制),而是 ...

  2. 基于 Egg.js 框架的 Node.js 服务构建之用户管理设计

    前言 近来公司需要构建一套 EMM(Enterprise Mobility Management)的管理平台,就这种面向企业的应用管理本身需要考虑的需求是十分复杂的,技术层面管理端和服务端构建是架构核 ...

  3. Nodejs之MEAN栈开发(八)---- 用户认证与会话管理详解

    用户认证与会话管理基本上是每个网站必备的一个功能.在Asp.net下做的比较多,大体的思路都是先根据用户提供的用户名和密码到数据库找到用户信息,然后校验,校验成功之后记住用户的姓名和相关信息,这个信息 ...

  4. flask设置cookie,设置session,模拟用户认证、模拟管理后台admin、模拟用户logout

    设置cookie HTTP协议是无状态的,在一次请求响应结束后,服务器不会留下关于客户端状态的信息.但是对于某些web程序来说,客户端的信息有必要被记住,比如用户的登录状态,这样就可以根据用户的状态来 ...

  5. mongodb之用户/认证/角色/权限管理

    前言 用户权限管理很重要,只给需要的权限,防止应用系统漏洞导致脱库 认证和授权 Authentication 认证识别,解决我是谁 Authorization 操作授权,我能做什么 认证机制 MONG ...

  6. Laravel 6 – 搭建管理后台的用户认证“脚手架”工具

    1. 下载Laravel/ui 命令: composer require laravel/ui "^1.0" -dev 注意laravel framework 6只支持版本1的la ...

  7. Cassandra--启用用户认证和用户管理

    ======================================================== 启用用户认证和创建超级用户 需要针对每个节点进行配置修改和重启,但授权操作仅需要在任一 ...

  8. apache用户认证访问机制(转)

    Apache服务器已经内置用户验证机制,大家只要适当的加以设置,便可以控制网站的某些部分要用户验证.前期准备,必须已经安装apache,如果还没安装,或者对安装很模糊的话,第1步:我们在/var/ww ...

  9. laravel 的用户认证

    1.简介 Laravel 中实现用户认证非常简单.实际上,几乎所有东西都已经为你配置好了.配置文件位于config/auth.php,其中包含了用于调整认证服务行为的.文档友好的选项配置. 在底层代码 ...

  10. 基于Ajax与用户认证系统的登录验证

    一.登录页面 from django.contrib import admin from django.urls import path from blog import views urlpatte ...

随机推荐

  1. 微软NewBing真是Niubility

    这是本人2012年的拙作:           晨兮,闻风雨,后而雷鸣电闪.迟不可再三,若故无食.然何如耶?雨大风狂,单车奈何?公交卡空,恐时不予我也.不免叹也,天亦不予我!         而后出, ...

  2. iText5实现Java生成PDF

    参考代码:https://github.com/youcong1996/study_simple_demo 参考博客:https://blog.csdn.net/weixin_37848710/art ...

  3. 初识Node和内置模块

    初识Node与内置模块 概述:了解Node.js,熟悉内置模块:fs模块.path模块.http模块 初识Node.js 浏览器中的JavaScript运行环境 运行环境是指代码正常运行所需的必要环境 ...

  4. 实现hypothesis在网页标注后同步到本地obsidian

    实现hypothesis在网页标注后同步到本地obsidian 遇到的question 2023.3.21日 在更改了自己的模板之后,可以能按照Todo的方式展现所有的标记,但是发现在同一个网页上增加 ...

  5. PHP微信三方平台-代公众号发起微信支付(jsAPI)

    一.前期准备工作 1.微信公众号需要开通微信支付认证将获取的秘钥给三方平台 2.添加支付回调域名地址:填写三方平台域名地址即可(最多5个) 二.代码demo 1.完成支付类 <?php /** ...

  6. $\mathcal{Friends\,\,Of\,--Mathic}$

  7. 阿里巴巴为什么这样强制从List中删除元素

    还是先举个例子,你侄女对天文知识感兴趣,然后你就用程序写了太阳系九大星系(水星.金星.地球.火星.木星.土星.天王星.海王星.冥王星)的运行轨迹图,然后拿给侄女看.然后她说错了错了,你的知识太旧了,多 ...

  8. Ansible 安装并简单使用

    Ansible 简介 Ansible 是一款 IT 自动化工具.主要应用场景有配置系统.软件部署.持续发布及不停服平滑滚动更新的高级任务编排. Ansible 本身非常简单易用,同时注重安全和可靠性, ...

  9. [MySQL]set autocommit=0与start transaction的区别[转载]

    set autocommit=0指事务非自动提交,自此句执行以后,每个SQL语句或者语句块所在的事务都需要显示"commit"才能提交事务. 1.不管autocommit 是1还是 ...

  10. 【FINALE】NOIP2022 退役记 || THE END.

    我的停课生活相册 - password:1007 目录 Day -4 Day -2 Day -1 Day 1 2022/11/26 NOIP 2022 OI 浅忆录 Day -4 好冷啊.有了那么点冬 ...