Socks 協定是一種代理 (Proxy) 協定, 例如我們所熟知的 Shdowsocks 便是 Socks 協定的一個典型應用程式, Socks 協定有多個版本, 目前最新的版本為 5, 其協定標準檔案為 RFC 1928。
我們一起來使用.net 7 構建一個支援使用者管理的高效能socks5代理伺服器端
VERSION | METHODS_COUNT | METHODS |
---|---|---|
1位元組 | 1位元組 | 1到255位元組,長度zMETHODS_COUNT |
0x05 | 0x03 | 0x00 0x01 0x02 |
METHODS列表(其他的認證方法可以自行上網瞭解)
VERSION | METHOD |
---|---|
1位元組 | 1位元組 |
0x05 | 0x00 |
VERSION | METHOD |
---|---|
1位元組 | 1位元組 |
0x05 | 0x02 |
VERSION | USERNAME_LENGTH | USERNAME | PASSWORD_LENGTH | PASSWORD |
---|---|---|---|---|
1位元組 | 1位元組 | 1到255位元組 | 1位元組 | 1到255位元組 |
0x01 | 0x01 | 0x0a | 0x01 | 0x0a |
VERSION | STATUS |
---|---|
1位元組 | 1位元組 |
0x01 | 0x00 |
VERSION | COMMAND | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1位元組 | 1位元組 | 1位元組 | 1位元組 | 1-255位元組 | 2位元組 |
VERSION | RESPONSE | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1位元組 | 1位元組 | 1位元組 | 1位元組 | 1-255位元組 | 2位元組 |
第3步成功後,進入資料轉發階段
RSV | FRAG | ADDRESS_TYPE | DST.ADDR | DST.PORT | DATA |
---|---|---|---|---|---|
2位元組 | 1位元組 | 1位元組 | 可變長 | 2位元組 | 可變長 |
從協定中我們可以看出,一個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