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. DVWA-Command Injection(命令执行)

    命令执行漏洞,顾名思义,服务端在进行一些网站的操作.管理的时候,需要调用系统命令,如果对传入的命令参数没有进行一些过滤,可以直接执行服务器系统的命令终端 LOW 审计源码 <?php // 判断 ...

  2. 在golang中如何正确判断接口是否为nil

    本文主要来分析一下在golang中,如何判断interface是否为nil,以及相关注意事项. 正常情况下,我们声明一个interface类型的变量,默认值将会返回nil,以golang自带的io.W ...

  3. oracle学习笔记1 安装 虚拟机 plsql 连接 oracle

    第一步就是安装 为了节省资源,运行起来更快捷,首先是在电脑上安装好vm虚拟机, 新建虚拟机,安装xp,也就是把xp光盘文件导入, 接着在虚拟机中下载oracle,解压的话会用到WinRAR,也一并导入 ...

  4. salesforce零基础学习(一百二十八)Durable Id获取以及相关概念浅入浅出

    本篇参考: salesforce 零基础开发入门学习(十一)sObject及Schema深入 https://developer.salesforce.com/docs/atlas.en-us.api ...

  5. Java 面试——Zookeeper

    一.Zookeeper 实现机制 文件系统 + 通知机制 二.Zookeeper 高可用实现原理 一个 ZooKeeper 集群如果要对外提供可用的服务,那么集群中必须要有过半的机器正常工作并且彼此之 ...

  6. Xenomai 再探

    一.Xenomai API 接口使用总结 Alarm-操作: 在使用实时任务过程中,采用看门狗定时器进行延时操作时,会产生实时域到非实时域的上下文切换操作,从而导致实时线程实时性受到影响,具体如下: ...

  7. wsl 中 docker-compose 搭建 kafka 集群出现的外部访问错误

    在 wsl 中用 docker-compose 搭建了一台 zookeeper + 三台 broker 的 kafka 集群,使用的镜像是 bitnami/kafka,在按照镜像文档运行容器后,发现运 ...

  8. Prometheus服务发现之kubernetes_sd_config

    一.为什么要使用Prometheus服务发现 之前我们讲过通过配置prometheus-operator的CRD ServiceMonitor来达到K8S集群相关组件和微服务的监控的目的,可以在Ser ...

  9. Linux中Python自动输入sudo 密码【管道 sudo参数 stdin&stdout】

    一.背景和需求 背景: 由于docker服务进程都是以root帐号的身份运行的,所以用docker跑abpred出来的文件所有者都是root, 而我作为一般用户,操作这个文件不够权限,运行代码时需要s ...

  10. IDA 逆 WDF 驱动时的函数识别插件

    快一年没更新了,累,工作累,各种累,想换个工作,突然发现找不到合适的工作了,哎,自己往火坑里跳,怪不得别人. import idautils import idaapi import idc prin ...