Socks 協(xié)議是一種代理 (Proxy) 協(xié)議, 例如我們所熟知的 Shdowsocks 便是 Socks 協(xié)議的一個典型應(yīng)用程序, Socks 協(xié)議有多個版本, 目前最新的版本為 5, 其協(xié)議標(biāo)準(zhǔn)文檔為 RFC 1928。
我們一起來使用.net 7 構(gòu)建一個支持用戶管理的高性能socks5代理服務(wù)端。
協(xié)議流程
1 client -> server 客戶端與服務(wù)端握手
VERSION | METHODS_COUNT | METHODS |
---|
1字節(jié) | 1字節(jié) | 1到255字節(jié),長度zMETHODS_COUNT |
0x05 | 0x03 | 0x00 0x01 0x02 |
- VERSION SOCKS協(xié)議版本,目前固定0x05
- METHODS_COUNT 客戶端支持的認(rèn)證方法數(shù)量
- METHODS 客戶端支持的認(rèn)證方法,每個方法占用1個字節(jié)
METHODS列表(其他的認(rèn)證方法可以自行上網(wǎng)了解)
- 0x00 不需要認(rèn)證(常用)
- 0x02 賬號密碼認(rèn)證(常用)
2.1 server -> client 無需認(rèn)證,直接進(jìn)入第3步,命令過程
VERSION | METHOD |
---|
1字節(jié) | 1字節(jié) |
0x05 | 0x00 |
2.2、server -> client 密碼認(rèn)證
VERSION | METHOD |
---|
1字節(jié) | 1字節(jié) |
0x05 | 0x02 |
2.2.1、client -> server 客戶端發(fā)送賬號密碼
VERSION | USERNAME_LENGTH | USERNAME | PASSWORD_LENGTH | PASSWORD |
---|
1字節(jié) | 1字節(jié) | 1到255字節(jié) | 1字節(jié) | 1到255字節(jié) |
0x01 | 0x01 | 0x0a | 0x01 | 0x0a |
- VERSION 認(rèn)證子協(xié)商版本(與SOCKS協(xié)議版本的0x05無關(guān)系)
- USERNAME_LENGTH 用戶名長度
- USERNAME 用戶名字節(jié)數(shù)組,長度為USERNAME_LENGTH
- PASSWORD_LENGTH 密碼長度
- PASSWORD 密碼字節(jié)數(shù)組,長度為PASSWORD_LENGTH
2.2.2、server -> client 返回認(rèn)證結(jié)果
VERSION | STATUS |
---|
1字節(jié) | 1字節(jié) |
0x01 | 0x00 |
- VERSION 認(rèn)證子協(xié)商版本
- STATUS 認(rèn)證結(jié)果,0x00認(rèn)證成功,大于0x00認(rèn)證失敗
3.1 client -> server 發(fā)送連接請求
VERSION | COMMAND | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|
1字節(jié) | 1字節(jié) | 1字節(jié) | 1字節(jié) | 1-255字節(jié) | 2字節(jié) |
- VERSION SOCKS協(xié)議版本,固定0x05
- COMMAND 命令
- 0x01 CONNECT 連接上游服務(wù)器
- 0x02 BIND 綁定,客戶端會接收來自代理服務(wù)器的鏈接,著名的FTP被動模式
- 0x03 UDP ASSOCIATE UDP中繼
- RSV 保留字段
- ADDRESS_TYPE 目標(biāo)服務(wù)器地址類型
- 0x01 IP V4地址
- 0x03 域名地址(沒有打錯,就是沒有0x02),域名地址的第1個字節(jié)為域名長度,剩下字節(jié)為域名名稱字節(jié)數(shù)組
- 0x04 IP V6地址
- DST.ADDR 目標(biāo)服務(wù)器地址(如果COMMAND是0x03,即UDP模式,此處為客戶端啟動UDP發(fā)送消息的主機(jī)地址)
- DST.PORT 目標(biāo)服務(wù)器端口(如果COMMAND是0x03,即UDP模式,此處為客戶端啟動UDP發(fā)送消息的端口)
3.2 server -> client 服務(wù)端響應(yīng)連接結(jié)果
VERSION | RESPONSE | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|
1字節(jié) | 1字節(jié) | 1字節(jié) | 1字節(jié) | 1-255字節(jié) | 2字節(jié) |
- VERSION SOCKS協(xié)議版本,固定0x05
- RESPONSE 響應(yīng)命令,除0x00外,其它響應(yīng)都應(yīng)該直接斷開連接
- 0x00 代理服務(wù)器連接目標(biāo)服務(wù)器成功
- 0x01 代理服務(wù)器故障
- 0x02 代理服務(wù)器規(guī)則集不允許連接
- 0x03 網(wǎng)絡(luò)無法訪問
- 0x04 目標(biāo)服務(wù)器無法訪問(主機(jī)名無效)
- 0x05 連接目標(biāo)服務(wù)器被拒絕
- 0x06 TTL已過期
- 0x07 不支持的命令
- 0x08 不支持的目標(biāo)服務(wù)器地址類型
- 0x09 - 0xFF 未分配
- RSV 保留字段
- BND.ADDR 代理服務(wù)器連接目標(biāo)服務(wù)器成功后的代理服務(wù)器IP
- BND.PORT 代理服務(wù)器連接目標(biāo)服務(wù)器成功后的代理服務(wù)器端口
4、數(shù)據(jù)轉(zhuǎn)發(fā)
第3步成功后,進(jìn)入數(shù)據(jù)轉(zhuǎn)發(fā)階段
- CONNECT 則將client過來的數(shù)據(jù)原樣轉(zhuǎn)發(fā)到目標(biāo),接著再將目標(biāo)回來的數(shù)據(jù)原樣返回給client
- BIND
- UDP ASSOCIATE
udp轉(zhuǎn)發(fā)的數(shù)據(jù)包
- 收到客戶端udp數(shù)據(jù)包后,解析出目標(biāo)地址,數(shù)據(jù),然后把數(shù)據(jù)發(fā)送過去
- 收到服務(wù)端回來的udp數(shù)據(jù)后,根據(jù)相同格式,打包,然后發(fā)回客戶端
RSV | FRAG | ADDRESS_TYPE | DST.ADDR | DST.PORT | DATA |
---|
2字節(jié) | 1字節(jié) | 1字節(jié) | 可變長 | 2字節(jié) | 可變長 |
- RSV 保留為
- FRAG 分片位
- ATYP 地址類型
- 0x01 IP V4地址
- 0x03 域名地址(沒有打錯,就是沒有0x02),域名地址的第1個字節(jié)為域名長度,剩下字節(jié)為域名名稱字節(jié)數(shù)組
- 0x04 IP V6地址
- DST.ADDR 目標(biāo)地址
- DST.PORT 目標(biāo)端口
- DATA 數(shù)據(jù)
狀態(tài)機(jī)控制每個連接狀態(tài)
從協(xié)議中我們可以看出,一個Socks5協(xié)議的連接需要經(jīng)過握手,認(rèn)證(可選),建立連接三個流程。那么這是典型的符合狀態(tài)機(jī)模型的業(yè)務(wù)流程。
創(chuàng)建狀態(tài)和事件枚舉
public enum ClientState
{
Normal,
ToBeCertified,
Certified,
Connected,
Death
}
public enum ClientStateEvents
{
OnRevAuthenticationNegotiation,
OnRevClientProfile,
OnRevRequestProxy,
OnException,
OnDeath
}
根據(jù)服務(wù)器是否配置需要用戶名密碼登錄,從而建立正確的狀態(tài)流程。
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);
在狀態(tài)扭轉(zhuǎn)中如果出現(xiàn)異常,則直接跳轉(zhuǎn)狀態(tài)到“Death”,
_machine.TransitionExceptionThrown += async (obj, e) =>
{
_logger.LogError(e.Exception.ToString());
await _machine.Fire(ClientStateEvents.OnException);
};
對應(yīng)狀態(tài)扭轉(zhuǎn)創(chuàng)建相應(yīng)的處理方法, 基本都是解析客戶端發(fā)來的數(shù)據(jù)包,判斷是否合理,最后返回一個響應(yīng)。
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)
{
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) });
}
public async Task HandleClientProfileAsync(UserToken token)
{
var version = token.ClientData.Span[0];
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 });
}
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)
{
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];
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)
{
token.ClientUdpEndPoint = new IPEndPoint(proxyInfo.Item2, proxyInfo.Item3);
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];
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.");
}
}
連接與用戶管理
當(dāng)服務(wù)器采用需要認(rèn)證的配置時,我們會返回給客戶端0x02的認(rèn)證方式,此時,客戶端需要上傳用戶名和密碼,如果認(rèn)證成功我們就可以將用戶信息與連接對象做綁定,方便后續(xù)管理。
在客戶端通過tcp或者udp上傳數(shù)據(jù)包,需要代理服務(wù)器轉(zhuǎn)發(fā)時,我們記錄數(shù)據(jù)包的大小作為上傳數(shù)據(jù)包流量記錄下來,反之亦然。
示例:記錄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);
}
當(dāng)管理界面修改某用戶的密碼或者過期時間的時候
1.修改密碼,強(qiáng)制目前所有使用該用戶名密碼的連接斷開
2.我們每個連接會有一個定時服務(wù),判斷是否過期
從而實現(xiàn)用戶下線。
public void UpdateUserPasswordAndExpireTime(string password, DateTime dateTime)
{
if (password != Password)
{
Dispose();
}
if (DateTime.Now > ExpireTime)
{
Dispose();
}
}
public void WhenExpireAutoOffline()
{
Task.Run(async () =>
{
while (true)
{
if (DateTime.Now > ExpireTime)
{
Dispose();
}
await Task.Delay(1000);
}
}, CancellationTokenSource.Token);
}
持久化
用戶數(shù)據(jù)包括,用戶名密碼,使用流量,過期時間等存儲在server端的sqlite數(shù)據(jù)庫中。通過EFcore來增刪改查。
如下定期更新用戶流量到數(shù)據(jù)庫
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);
}
});
}
public async Task BatchUpdateUserFlowrateAsync(IEnumerable<User> users)
{
using (var context = _dbContextFactory.CreateDbContext())
{
context.Users.UpdateRange(users);
await context.SaveChangesAsync();
}
}
效果示例
打開服務(wù)

打開Proxifier配置到我們的服務(wù)

查看Proxifier已經(jīng)流量走到我們的服務(wù)

服務(wù)端管理器

源碼以及如何使用
https://github.com/BruceQiu1996/Socks5Server
轉(zhuǎn)自https://www.cnblogs.com/qwqwQAQ/p/17410319.html
?
該文章在 2025/5/12 9:31:27 編輯過