A Core/NosSmooth.Cryptography/ClientLoginCryptography.cs => Core/NosSmooth.Cryptography/ClientLoginCryptography.cs +41 -0
@@ 0,0 1,41 @@
+//
+// ClientLoginCryptography.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+
+namespace NosSmooth.Cryptography;
+
+/// <summary>
+/// A cryptography used for logging to NosTale server from the client.
+/// </summary>
+public class ClientLoginCryptography : ICryptography
+{
+ private static readonly Random Random = new Random(DateTime.Now.Millisecond);
+
+ /// <inheritdoc />
+ public string Decrypt(in ReadOnlySpan<byte> bytes, Encoding encoding)
+ {
+ var output = new StringBuilder();
+ foreach (var c in bytes)
+ {
+ output.Append(Convert.ToChar(c - 0xF));
+ }
+
+ return output.ToString();
+ }
+
+ /// <inheritdoc />
+ public byte[] Encrypt(string value, Encoding encoding)
+ {
+ var output = new byte[value.Length + 1];
+ for (int i = 0; i < value.Length; i++)
+ {
+ output[i] = (byte)((value[i] ^ 0xC3) + 0xF);
+ }
+ output[output.Length - 1] = 0xD8;
+ return output;
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Cryptography/ClientWorldCryptography.cs => Core/NosSmooth.Cryptography/ClientWorldCryptography.cs +311 -0
@@ 0,0 1,311 @@
+//
+// ClientWorldCryptography.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+
+namespace NosSmooth.Cryptography;
+
+/// <summary>
+/// A cryptography used on world server, has to have a session id (encryption key) set from the client.
+/// </summary>
+public class ClientWorldCryptography : ICryptography
+{
+ private static readonly char[] Keys = { ' ', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'n' };
+
+ /// <summary>
+ /// Gets or sets the encryption key.
+ /// </summary>
+ public int EncryptionKey { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClientWorldCryptography"/> class.
+ /// </summary>
+ /// <param name="encryptionKey">Encryption key received by LoginServer.</param>
+ public ClientWorldCryptography(int encryptionKey = 0)
+ {
+ EncryptionKey = encryptionKey;
+ }
+
+ /// <inheritdoc />
+ public string Decrypt(in ReadOnlySpan<byte> bytes, Encoding encoding)
+ {
+ int index = 0;
+ var currentPacket = new StringBuilder();
+
+ while (index < bytes.Length)
+ {
+ byte currentByte = bytes[index++];
+
+ if (currentByte == 0xFF)
+ {
+ currentPacket.Append('\n');
+ continue;
+ }
+
+ int length = currentByte & 0x7F;
+
+ if ((currentByte & 0x80) != 0)
+ {
+ while (length != 0)
+ {
+ if (index < bytes.Length)
+ {
+ currentByte = bytes[index++];
+ int firstIndex = (currentByte & 0xF0) >> 4;
+ char first = '?';
+ if (firstIndex != 0)
+ {
+ firstIndex--;
+ first = firstIndex != 14 ? Keys[firstIndex] : '\u0000';
+ }
+
+ if (first != 0x6E)
+ {
+ currentPacket.Append(first);
+ }
+
+ if (length <= 1)
+ {
+ break;
+ }
+
+ int secondIndex = currentByte & 0xF;
+ char second = '?';
+ if (secondIndex != 0)
+ {
+ secondIndex--;
+ second = secondIndex != 14 ? Keys[secondIndex] : '\u0000';
+ }
+
+ if (second != 0x6E)
+ {
+ currentPacket.Append(second);
+ }
+
+ length -= 2;
+ }
+ else
+ {
+ length--;
+ }
+ }
+ }
+ else
+ {
+ while (length != 0)
+ {
+ if (index < bytes.Length)
+ {
+ currentPacket.Append((char)(bytes[index] ^ 0xFF));
+ index++;
+ }
+ else if (index == bytes.Length)
+ {
+ currentPacket.Append((char)0xFF);
+ index++;
+ }
+
+ length--;
+ }
+ }
+ }
+
+ return currentPacket.ToString();
+ }
+
+ /// <inheritdoc />
+ public byte[] Encrypt(string value, Encoding encoding)
+ {
+ var output = new List<byte>();
+
+ string mask = new string
+ (
+ value.Select
+ (
+ c =>
+ {
+ sbyte b = (sbyte)c;
+ if (c == '#' || c == '/' || c == '%')
+ {
+ return '0';
+ }
+
+ if ((b -= 0x20) == 0 || (b += unchecked((sbyte)0xF1)) < 0 || (b -= 0xB) < 0 ||
+ b - unchecked((sbyte)0xC5) == 0)
+ {
+ return '1';
+ }
+
+ return '0';
+ }
+ ).ToArray()
+ );
+
+ int packetLength = value.Length;
+
+ int sequenceCounter = 0;
+ int currentPosition = 0;
+
+ while (currentPosition <= packetLength)
+ {
+ int lastPosition = currentPosition;
+ while (currentPosition < packetLength && mask[currentPosition] == '0')
+ {
+ currentPosition++;
+ }
+
+ int sequences;
+ int length;
+
+ if (currentPosition != 0)
+ {
+ length = currentPosition - lastPosition;
+ sequences = length / 0x7E;
+ for (int i = 0; i < length; i++, lastPosition++)
+ {
+ if (i == sequenceCounter * 0x7E)
+ {
+ if (sequences == 0)
+ {
+ output.Add((byte)(length - i));
+ }
+ else
+ {
+ output.Add(0x7E);
+ sequences--;
+ sequenceCounter++;
+ }
+ }
+
+ output.Add((byte)((byte)value[lastPosition] ^ 0xFF));
+ }
+ }
+
+ if (currentPosition >= packetLength)
+ {
+ break;
+ }
+
+ lastPosition = currentPosition;
+ while (currentPosition < packetLength && mask[currentPosition] == '1')
+ {
+ currentPosition++;
+ }
+
+ if (currentPosition == 0)
+ {
+ continue;
+ }
+
+ length = currentPosition - lastPosition;
+ sequences = length / 0x7E;
+ for (int i = 0; i < length; i++, lastPosition++)
+ {
+ if (i == sequenceCounter * 0x7E)
+ {
+ if (sequences == 0)
+ {
+ output.Add((byte)((length - i) | 0x80));
+ }
+ else
+ {
+ output.Add(0x7E | 0x80);
+ sequences--;
+ sequenceCounter++;
+ }
+ }
+
+ byte currentByte = (byte)value[lastPosition];
+ switch (currentByte)
+ {
+ case 0x20:
+ currentByte = 1;
+ break;
+ case 0x2D:
+ currentByte = 2;
+ break;
+ case 0xFF:
+ currentByte = 0xE;
+ break;
+ default:
+ currentByte -= 0x2C;
+ break;
+ }
+
+ if (currentByte == 0x00)
+ {
+ continue;
+ }
+
+ if (i % 2 == 0)
+ {
+ output.Add((byte)(currentByte << 4));
+ }
+ else
+ {
+ output[output.Count - 1] = (byte)(output.Last() | currentByte);
+ }
+ }
+ }
+
+ output.Add(0xFF);
+
+ sbyte sessionNumber = (sbyte)((EncryptionKey >> 6) & 0xFF & 0x80000003);
+
+ if (sessionNumber < 0)
+ {
+ sessionNumber = (sbyte)(((sessionNumber - 1) | 0xFFFFFFFC) + 1);
+ }
+
+ byte sessionKey = (byte)(EncryptionKey & 0xFF);
+
+ if (EncryptionKey != 0)
+ {
+ sessionNumber = -1;
+ }
+
+ switch (sessionNumber)
+ {
+ case 0:
+ for (int i = 0; i < output.Count; i++)
+ {
+ output[i] = (byte)(output[i] + sessionKey + 0x40);
+ }
+
+ break;
+ case 1:
+ for (int i = 0; i < output.Count; i++)
+ {
+ output[i] = (byte)(output[i] - (sessionKey + 0x40));
+ }
+
+ break;
+ case 2:
+ for (int i = 0; i < output.Count; i++)
+ {
+ output[i] = (byte)((output[i] ^ 0xC3) + sessionKey + 0x40);
+ }
+
+ break;
+ case 3:
+ for (int i = 0; i < output.Count; i++)
+ {
+ output[i] = (byte)((output[i] ^ 0xC3) - (sessionKey + 0x40));
+ }
+
+ break;
+ default:
+ for (int i = 0; i < output.Count; i++)
+ {
+ output[i] = (byte)(output[i] + 0x0F);
+ }
+
+ break;
+ }
+
+ return output.ToArray();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Cryptography/CryptographyManager.cs => Core/NosSmooth.Cryptography/CryptographyManager.cs +57 -0
@@ 0,0 1,57 @@
+//
+// CryptographyManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Cryptography;
+
+/// <summary>
+/// A storage of server and client cryptography.
+/// </summary>
+public class CryptographyManager
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptographyManager"/> class.
+ /// </summary>
+ public CryptographyManager()
+ {
+ ServerWorld = new ServerWorldCryptography(0);
+ ServerLogin = new ServerLoginCryptography();
+ ClientLogin = new ClientLoginCryptography();
+ ClientWorld = new ClientWorldCryptography(0);
+ }
+
+ /// <summary>
+ /// Gets the cryptography for server world.
+ /// </summary>
+ public ICryptography ServerWorld { get; }
+
+ /// <summary>
+ /// Gets the cryptography for server login.
+ /// </summary>
+ public ICryptography ServerLogin { get; }
+
+ /// <summary>
+ /// Gets the cryptography for client world.
+ /// </summary>
+ public ICryptography ClientWorld { get; }
+
+ /// <summary>
+ /// Gets the cryptography for client login.
+ /// </summary>
+ public ICryptography ClientLogin { get; }
+
+ /// <summary>
+ /// Gets or sets the encryption key of the connection.
+ /// </summary>
+ public int EncryptionKey
+ {
+ get => ((ServerWorldCryptography)ServerWorld).EncryptionKey;
+ set
+ {
+ ((ServerWorldCryptography)ServerWorld).EncryptionKey = value;
+ ((ClientWorldCryptography)ClientWorld).EncryptionKey = value;
+ }
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Cryptography/ICryptography.cs => Core/NosSmooth.Cryptography/ICryptography.cs +31 -0
@@ 0,0 1,31 @@
+//
+// ICryptography.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+
+namespace NosSmooth.Cryptography;
+
+/// <summary>
+/// An intefrace for NosTale cryptography, encryption, decryption of packets.
+/// </summary>
+public interface ICryptography
+{
+ /// <summary>
+ /// Decrypt the raw packet (byte array) to a readable list string.
+ /// </summary>
+ /// <param name="str">Bytes to decrypt.</param>
+ /// <param name="encoding">The encoding.</param>
+ /// <returns>Decrypted packet to string list.</returns>
+ string Decrypt(in ReadOnlySpan<byte> str, Encoding encoding);
+
+ /// <summary>
+ /// Encrypt the string packet to byte array.
+ /// </summary>
+ /// <param name="packet">String to encrypt.</param>
+ /// <param name="encoding">The encoding.</param>
+ /// <returns>Encrypted packet as byte array.</returns>
+ byte[] Encrypt(string packet, Encoding encoding);
+}<
\ No newline at end of file
A Core/NosSmooth.Cryptography/NosSmooth.Cryptography.csproj => Core/NosSmooth.Cryptography/NosSmooth.Cryptography.csproj +9 -0
@@ 0,0 1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+</Project>
A Core/NosSmooth.Cryptography/ServerLoginCryptography.cs => Core/NosSmooth.Cryptography/ServerLoginCryptography.cs +62 -0
@@ 0,0 1,62 @@
+//
+// ServerLoginCryptography.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+
+namespace NosSmooth.Cryptography;
+
+/// <summary>
+/// A cryptography used for logging to NosTale server from the server.
+/// </summary>
+public class ServerLoginCryptography : ICryptography
+{
+ /// <inheritdoc />
+ public string Decrypt(in ReadOnlySpan<byte> str, Encoding encoding)
+ {
+ try
+ {
+ string decryptedPacket = string.Empty;
+
+ foreach (byte character in str)
+ {
+ if (character > 14)
+ {
+ decryptedPacket += Convert.ToChar((character - 15) ^ 195);
+ }
+ else
+ {
+ decryptedPacket += Convert.ToChar((256 - (15 - character)) ^ 195);
+ }
+ }
+
+ return decryptedPacket;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ /// <inheritdoc />
+ public byte[] Encrypt(string packet, Encoding encoding)
+ {
+ try
+ {
+ packet += " ";
+ byte[] tmp = Encoding.Default.GetBytes(packet);
+ for (int i = 0; i < packet.Length; i++)
+ {
+ tmp[i] = Convert.ToByte(tmp[i] + 15);
+ }
+ tmp[tmp.Length - 1] = 25;
+ return tmp;
+ }
+ catch
+ {
+ return Array.Empty<byte>();
+ }
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Cryptography/ServerWorldCryptography.cs => Core/NosSmooth.Cryptography/ServerWorldCryptography.cs +282 -0
@@ 0,0 1,282 @@
+//
+// ServerWorldCryptography.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+
+namespace NosSmooth.Cryptography;
+
+/// <summary>
+/// A cryptography used on world server, has to have a session id (encryption key) set from the world.
+/// </summary>
+public class ServerWorldCryptography : ICryptography
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ServerWorldCryptography"/> class.
+ /// </summary>
+ /// <param name="encryptionKey">The encryption key.</param>
+ public ServerWorldCryptography(int encryptionKey)
+ {
+ EncryptionKey = encryptionKey;
+ }
+
+ /// <summary>
+ /// Gets or sets the encryption key.
+ /// </summary>
+ public int EncryptionKey { get; set; }
+
+ /// <inheritdoc />
+ public string Decrypt(in ReadOnlySpan<byte> str, Encoding encoding)
+ {
+ if (EncryptionKey == 0)
+ {
+ return DecryptUnauthed(str);
+ }
+
+ return DecryptAuthed(str, EncryptionKey, encoding);
+ }
+
+ /// <inheritdoc />
+ public byte[] Encrypt(string packet, Encoding encoding)
+ {
+ byte[] strBytes = encoding.GetBytes(packet);
+ int bytesLength = strBytes.Length;
+
+ byte[] encryptedData = new byte[bytesLength + (int)Math.Ceiling((decimal)bytesLength / 0x7E) + 1];
+
+ int ii = 0;
+ for (int i = 0; i < bytesLength; i++)
+ {
+ if (i % 0x7E == 0)
+ {
+ encryptedData[i + ii] = (byte)(bytesLength - i > 0x7E ? 0x7E : bytesLength - i);
+ ii++;
+ }
+ encryptedData[i + ii] = (byte)~strBytes[i];
+ }
+ encryptedData[encryptedData.Length - 1] = 0xFF;
+
+ return encryptedData;
+ }
+
+ private static string DecryptAuthed(in ReadOnlySpan<byte> str, int encryptionKey, Encoding encoding)
+ {
+ var encryptedString = new StringBuilder();
+
+ int sessionKey = encryptionKey & 0xFF;
+ byte sessionNumber = unchecked((byte)(encryptionKey >> 6));
+ sessionNumber &= 0xFF;
+ sessionNumber &= 3;
+
+ switch (sessionNumber)
+ {
+ case 0:
+ foreach (byte character in str)
+ {
+ byte firstbyte = unchecked((byte)(sessionKey + 0x40));
+ byte highbyte = unchecked((byte)(character - firstbyte));
+ encryptedString.Append((char)highbyte);
+ }
+
+ break;
+
+ case 1:
+ foreach (byte character in str)
+ {
+ byte firstbyte = unchecked((byte)(sessionKey + 0x40));
+ byte highbyte = unchecked((byte)(character + firstbyte));
+ encryptedString.Append((char)highbyte);
+ }
+
+ break;
+
+ case 2:
+ foreach (byte character in str)
+ {
+ byte firstbyte = unchecked((byte)(sessionKey + 0x40));
+ byte highbyte = unchecked((byte)(character - firstbyte ^ 0xC3));
+ encryptedString.Append((char)highbyte);
+ }
+
+ break;
+
+ case 3:
+ foreach (byte character in str)
+ {
+ byte firstbyte = unchecked((byte)(sessionKey + 0x40));
+ byte highbyte = unchecked((byte)(character + firstbyte ^ 0xC3));
+ encryptedString.Append((char)highbyte);
+ }
+
+ break;
+
+ default:
+ encryptedString.Append((char)0xF);
+ break;
+ }
+
+ string[] temp = encryptedString.ToString().Split((char)0xFF);
+
+ var save = new StringBuilder();
+
+ for (int i = 0; i < temp.Length; i++)
+ {
+ save.Append(DecryptPrivate(temp[i].AsSpan(), encoding));
+ if (i < temp.Length - 2)
+ {
+ save.Append((char)'\n');
+ }
+ }
+
+ return save.ToString();
+ }
+
+ private static string DecryptPrivate(in ReadOnlySpan<char> str, Encoding encoding)
+ {
+ using var receiveData = new MemoryStream();
+ char[] table = { ' ', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\n' };
+ for (int count = 0; count < str.Length; count++)
+ {
+ if (str[count] <= 0x7A)
+ {
+ int len = str[count];
+
+ for (int i = 0; i < len; i++)
+ {
+ count++;
+
+ try
+ {
+ receiveData.WriteByte(unchecked((byte)(str[count] ^ 0xFF)));
+ }
+ catch
+ {
+ receiveData.WriteByte(255);
+ }
+ }
+ }
+ else
+ {
+ int len = str[count];
+ len &= 0x7F;
+
+ for (int i = 0; i < len; i++)
+ {
+ count++;
+ int highbyte;
+ try
+ {
+ highbyte = str[count];
+ }
+ catch
+ {
+ highbyte = 0;
+ }
+
+ highbyte &= 0xF0;
+ highbyte >>= 0x4;
+
+ int lowbyte;
+ try
+ {
+ lowbyte = str[count];
+ }
+ catch
+ {
+ lowbyte = 0;
+ }
+
+ lowbyte &= 0x0F;
+
+ if (highbyte != 0x0 && highbyte != 0xF)
+ {
+ receiveData.WriteByte(unchecked((byte)table[highbyte - 1]));
+ i++;
+ }
+
+ if (lowbyte != 0x0 && lowbyte != 0xF)
+ {
+ receiveData.WriteByte(unchecked((byte)table[lowbyte - 1]));
+ }
+ }
+ }
+ }
+
+ byte[] tmp = Encoding.Convert(encoding, Encoding.UTF8, receiveData.ToArray());
+ return Encoding.UTF8.GetString(tmp);
+ }
+
+ private static string DecryptUnauthed(in ReadOnlySpan<byte> str)
+ {
+ try
+ {
+ var encryptedStringBuilder = new StringBuilder();
+ for (int i = 1; i < str.Length; i++)
+ {
+ if (Convert.ToChar(str[i]) == 0xE)
+ {
+ return encryptedStringBuilder.ToString();
+ }
+
+ int firstbyte = Convert.ToInt32(str[i] - 0xF);
+ int secondbyte = firstbyte;
+ secondbyte &= 240;
+ firstbyte = Convert.ToInt32(firstbyte - secondbyte);
+ secondbyte >>= 4;
+
+ switch (secondbyte)
+ {
+ case 0:
+ case 1:
+ encryptedStringBuilder.Append(' ');
+ break;
+
+ case 2:
+ encryptedStringBuilder.Append('-');
+ break;
+
+ case 3:
+ encryptedStringBuilder.Append('.');
+ break;
+
+ default:
+ secondbyte += 0x2C;
+ encryptedStringBuilder.Append(Convert.ToChar(secondbyte));
+ break;
+ }
+
+ switch (firstbyte)
+ {
+ case 0:
+ encryptedStringBuilder.Append(' ');
+ break;
+
+ case 1:
+ encryptedStringBuilder.Append(' ');
+ break;
+
+ case 2:
+ encryptedStringBuilder.Append('-');
+ break;
+
+ case 3:
+ encryptedStringBuilder.Append('.');
+ break;
+
+ default:
+ firstbyte += 0x2C;
+ encryptedStringBuilder.Append(Convert.ToChar(firstbyte));
+ break;
+ }
+ }
+
+ return encryptedStringBuilder.ToString();
+ }
+ catch (OverflowException)
+ {
+ return string.Empty;
+ }
+ }
+}<
\ No newline at end of file
M NosSmooth.sln => NosSmooth.sln +32 -0
@@ 54,6 54,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Extensions.Pathfi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Game.Tests", "Tests\NosSmooth.Game.Tests\NosSmooth.Game.Tests.csproj", "{21ECBA0F-38FA-45A5-8B42-9A76425204BC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Cryptography", "Core\NosSmooth.Cryptography\NosSmooth.Cryptography.csproj", "{9035E5AD-8B5F-46CA-B8E1-5273722B392D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pcap", "Pcap", "{B4224C41-FDB4-426A-83D8-C5AFB4F7D362}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Pcap", "Pcap\NosSmooth.Pcap\NosSmooth.Pcap.csproj", "{FD185689-EE0F-4403-A1EB-5511D2292CF4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ 268,6 274,30 @@ Global
{21ECBA0F-38FA-45A5-8B42-9A76425204BC}.Release|x64.Build.0 = Release|Any CPU
{21ECBA0F-38FA-45A5-8B42-9A76425204BC}.Release|x86.ActiveCfg = Release|Any CPU
{21ECBA0F-38FA-45A5-8B42-9A76425204BC}.Release|x86.Build.0 = Release|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Debug|x64.Build.0 = Debug|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Debug|x86.Build.0 = Debug|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Release|x64.ActiveCfg = Release|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Release|x64.Build.0 = Release|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Release|x86.ActiveCfg = Release|Any CPU
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D}.Release|x86.Build.0 = Release|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Debug|x64.Build.0 = Debug|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Debug|x86.Build.0 = Debug|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Release|x64.ActiveCfg = Release|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Release|x64.Build.0 = Release|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Release|x86.ActiveCfg = Release|Any CPU
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ 290,6 320,8 @@ Global
{21F7EA0B-5E3C-4016-8ADD-28AF37C00782} = {3A6D13E3-4BBA-4B3D-AE99-BAC8B375F7DF}
{564CAD6F-09B1-450B-83ED-9BCDE106B646} = {3A6D13E3-4BBA-4B3D-AE99-BAC8B375F7DF}
{21ECBA0F-38FA-45A5-8B42-9A76425204BC} = {C6A8760D-92CB-4307-88A7-36CCAEBA4AD1}
+ {9035E5AD-8B5F-46CA-B8E1-5273722B392D} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
+ {FD185689-EE0F-4403-A1EB-5511D2292CF4} = {B4224C41-FDB4-426A-83D8-C5AFB4F7D362}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5F46653-4DEC-429B-8580-4ED18ED9B4CA}
A Pcap/NosSmooth.Pcap/ConnectionData.cs => Pcap/NosSmooth.Pcap/ConnectionData.cs +28 -0
@@ 0,0 1,28 @@
+//
+// ConnectionData.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Net;
+
+namespace NosSmooth.Pcap;
+
+/// <summary>
+/// Data from a tcp connection containing first few sniffed packets.
+/// </summary>
+/// <param name="SourceAddress">The packets source address.</param>
+/// <param name="SourcePort">The packets source port.</param>
+/// <param name="DestinationAddress">The packets destination address.</param>
+/// <param name="DestinationPort">The packets destination port.</param>
+/// <param name="SniffedData">The sniffed data.</param>
+/// <param name="FirstObservedAt">The time first data were observed at.</param>
+public record ConnectionData
+(
+ IPAddress SourceAddress,
+ int SourcePort,
+ IPAddress DestinationAddress,
+ int DestinationPort,
+ List<byte[]> SniffedData,
+ DateTimeOffset FirstObservedAt
+);<
\ No newline at end of file
A Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj => Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj +18 -0
@@ 0,0 1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="SharpPcap" Version="6.2.5" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Core\NosSmooth.Core\NosSmooth.Core.csproj" />
+ <ProjectReference Include="..\..\Core\NosSmooth.Cryptography\NosSmooth.Cryptography.csproj" />
+ </ItemGroup>
+
+</Project>
A Pcap/NosSmooth.Pcap/PcapNostaleClient.cs => Pcap/NosSmooth.Pcap/PcapNostaleClient.cs +244 -0
@@ 0,0 1,244 @@
+//
+// PcapNostaleClient.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Packets;
+using NosSmooth.Cryptography;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+using PacketDotNet;
+using Remora.Results;
+using SharpPcap;
+using SharpPcap.LibPcap;
+
+namespace NosSmooth.Pcap;
+
+/// <summary>
+/// A NosTale client that works by capturing packets.
+/// </summary>
+/// <remarks>
+/// Sending packets means the same number of packet will appear twice.
+/// That may be detected by the server and the server may suspect
+/// something malicious is going on.
+/// </remarks>
+public class PcapNostaleClient : BaseNostaleClient
+{
+ private readonly Process _process;
+ private readonly Encoding _encoding;
+ private readonly PcapNostaleManager _pcapManager;
+ private readonly ProcessTcpManager _processTcpManager;
+ private readonly IPacketHandler _handler;
+ private CryptographyManager _crypto;
+ private int _localPort;
+ private long _localAddr;
+ private CancellationToken? _stoppingToken;
+ private bool _running;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PcapNostaleClient"/> class.
+ /// </summary>
+ /// <param name="process">The process to look for.</param>
+ /// <param name="encryptionKey">The current encryption key of the world connection, if known. Zero if unknown.</param>
+ /// <param name="encoding">The encoding.</param>
+ /// <param name="pcapManager">The pcap manager.</param>
+ /// <param name="processTcpManager">The process manager.</param>
+ /// <param name="handler">The packet handler.</param>
+ /// <param name="commandProcessor">The command processor.</param>
+ public PcapNostaleClient
+ (
+ Process process,
+ int encryptionKey,
+ Encoding encoding,
+ PcapNostaleManager pcapManager,
+ ProcessTcpManager processTcpManager,
+ IPacketHandler handler,
+ CommandProcessor commandProcessor
+ )
+ : base(commandProcessor)
+ {
+ _process = process;
+ _encoding = encoding;
+ _pcapManager = pcapManager;
+ _processTcpManager = processTcpManager;
+ _handler = handler;
+ _crypto = new CryptographyManager();
+ _crypto.EncryptionKey = encryptionKey;
+ }
+
+ /// <inheritdoc />
+ public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
+ {
+ if (_running)
+ {
+ return Result.FromSuccess();
+ }
+
+ _running = true;
+
+ _stoppingToken = stopRequested;
+ TcpConnection? lastConnection = null;
+ TcpConnection? reverseLastConnection = null;
+ try
+ {
+ await _processTcpManager.RegisterProcess(_process.Id);
+ _pcapManager.AddClient();
+
+ while (!stopRequested.IsCancellationRequested)
+ {
+ if (_process.HasExited)
+ {
+ break;
+ }
+
+ var connection = (await _processTcpManager.GetConnectionsAsync(_process.Id)).Cast<TcpConnection?>()
+ .FirstOrDefault();
+
+ if (lastConnection != connection)
+ {
+ if (lastConnection is not null)
+ {
+ _pcapManager.UnregisterConnection(lastConnection.Value);
+ _crypto.EncryptionKey = 0;
+ }
+ if (reverseLastConnection is not null)
+ {
+ _pcapManager.UnregisterConnection(reverseLastConnection.Value);
+ }
+
+ if (connection is not null)
+ {
+ var conn = connection.Value;
+ var reverseConn = new TcpConnection
+ (conn.RemoteAddr, conn.RemotePort, conn.LocalAddr, conn.LocalPort);
+
+ _localAddr = conn.LocalAddr;
+ _localPort = conn.LocalPort;
+
+ _pcapManager.RegisterConnection(conn, this);
+ _pcapManager.RegisterConnection(reverseConn, this);
+
+ lastConnection = conn;
+ reverseLastConnection = reverseConn;
+ }
+ else
+ {
+ lastConnection = null;
+ reverseLastConnection = null;
+ }
+ }
+
+ await Task.Delay(TimeSpan.FromMilliseconds(10), stopRequested);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // ignored
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ finally
+ {
+ await _processTcpManager.UnregisterProcess(_process.Id);
+ _pcapManager.RemoveClient();
+ if (lastConnection is not null)
+ {
+ _pcapManager.UnregisterConnection(lastConnection.Value);
+ }
+
+ if (reverseLastConnection is not null)
+ {
+ _pcapManager.UnregisterConnection(reverseLastConnection.Value);
+ }
+
+ _running = false;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public override Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public override Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Called when an associated packet has been obtained.
+ /// </summary>
+ /// <param name="connection">The connection that obtained the packet.</param>
+ /// <param name="payloadData">The raw payload data of the packet.</param>
+ internal void OnPacketArrival(TcpConnection connection, byte[] payloadData)
+ {
+ string data;
+ PacketSource source;
+ bool containsPacketId = false;
+
+ if (connection.LocalAddr == _localAddr && connection.LocalPort == _localPort)
+ { // sent packet
+ source = PacketSource.Client;
+ if (_crypto.EncryptionKey == 0)
+ {
+ var worldDecrypted = _crypto.ServerWorld.Decrypt(payloadData, _encoding).Trim();
+
+ var splitted = worldDecrypted.Split(' ');
+ if (splitted.Length == 2 && int.TryParse(splitted[1], out var encryptionKey))
+ { // possibly first packet from world
+ _crypto.EncryptionKey = encryptionKey;
+ data = worldDecrypted;
+ containsPacketId = true;
+ }
+ else
+ { // doesn't look like first packet from world, so assume login.
+ data = _crypto.ServerLogin.Decrypt(payloadData, _encoding);
+ }
+ }
+ else
+ {
+ data = _crypto.ServerWorld.Decrypt(payloadData, _encoding);
+ containsPacketId = true;
+ }
+ }
+ else
+ { // received packet
+ source = PacketSource.Server;
+ if (_crypto.EncryptionKey == 0)
+ { // probably login
+ data = _crypto.ClientLogin.Decrypt(payloadData, _encoding);
+ }
+ else
+ {
+ data = _crypto.ClientWorld.Decrypt(payloadData, _encoding);
+ }
+ }
+
+ if (data.Length > 0)
+ {
+ foreach (var line in data.Split('\n'))
+ {
+ var linePacket = line;
+ if (containsPacketId)
+ {
+ linePacket = line.Substring(line.IndexOf(' ') + 1);
+ }
+
+ _handler.HandlePacketAsync(this, source, linePacket.Trim(), _stoppingToken ?? default);
+ }
+
+ }
+ }
+}<
\ No newline at end of file
A Pcap/NosSmooth.Pcap/PcapNostaleManager.cs => Pcap/NosSmooth.Pcap/PcapNostaleManager.cs +233 -0
@@ 0,0 1,233 @@
+//
+// PcapNostaleManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Net;
+using Microsoft.Extensions.Logging;
+using SharpPcap;
+using SharpPcap.LibPcap;
+
+namespace NosSmooth.Pcap;
+
+/// <summary>
+/// Captures packets, distributes them to Pcap clients.
+/// </summary>
+public class PcapNostaleManager
+{
+ private readonly ILogger<PcapNostaleManager> _logger;
+ private readonly ConcurrentDictionary<TcpConnection, ConnectionData> _connections;
+ private readonly ConcurrentDictionary<TcpConnection, PcapNostaleClient> _clients;
+ private Task? _deletionTask;
+ private CancellationTokenSource? _deletionTaskCancellationSource;
+ private int _clientsCount;
+ private bool _started;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PcapNostaleManager"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ public PcapNostaleManager(ILogger<PcapNostaleManager> logger)
+ {
+ _logger = logger;
+ _connections = new ConcurrentDictionary<TcpConnection, ConnectionData>();
+ _clients = new ConcurrentDictionary<TcpConnection, PcapNostaleClient>();
+ }
+
+ /// <summary>
+ /// Add a pcap client.
+ /// </summary>
+ internal void AddClient()
+ {
+ var count = Interlocked.Increment(ref _clientsCount);
+
+ if (count == 1)
+ {
+ StartCapturing();
+ }
+ }
+
+ /// <summary>
+ /// Remove a pcap client.
+ /// </summary>
+ /// <remarks>
+ /// When no clients are left, packet capture will be stopped.
+ /// </remarks>
+ internal void RemoveClient()
+ {
+ var count = Interlocked.Decrement(ref _clientsCount);
+
+ if (count == 0)
+ {
+ Stop();
+ }
+ }
+
+ /// <summary>
+ /// Associate the given connection with the given client.
+ /// </summary>
+ /// <param name="connection">The connection to associate.</param>
+ /// <param name="client">The client to associate the connection with.</param>
+ internal void RegisterConnection(TcpConnection connection, PcapNostaleClient client)
+ {
+ _clients.AddOrUpdate(connection, (c) => client, (c1, c2) => client);
+
+ if (_connections.TryGetValue(connection, out var data))
+ {
+ foreach (var sniffedPacket in data.SniffedData)
+ {
+ client.OnPacketArrival(connection, sniffedPacket);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Disassociate the given connection.
+ /// </summary>
+ /// <param name="connection">The connection to disassociate.</param>
+ internal void UnregisterConnection(TcpConnection connection)
+ {
+ _clients.TryRemove(connection, out _);
+ }
+
+ private void Stop()
+ {
+ if (!_started)
+ {
+ return;
+ }
+
+ _started = false;
+ foreach (var device in LibPcapLiveDeviceList.Instance)
+ {
+ device.StopCapture();
+ }
+
+ var task = _deletionTask;
+ _deletionTask = null;
+
+ _deletionTaskCancellationSource?.Cancel();
+ _deletionTaskCancellationSource?.Dispose();
+ _deletionTaskCancellationSource = null;
+
+ task?.GetAwaiter().GetResult();
+ task?.Dispose();
+ }
+
+ /// <summary>
+ /// Start capturing packets from all devices.
+ /// </summary>
+ public void StartCapturing()
+ {
+ if (_started)
+ {
+ return;
+ }
+
+ _started = true;
+ _deletionTaskCancellationSource = new CancellationTokenSource();
+ _deletionTask = Task.Run(() => DeletionTask(_deletionTaskCancellationSource.Token));
+
+ foreach (var device in LibPcapLiveDeviceList.Instance)
+ {
+ if (!device.Opened)
+ {
+ device.Open();
+ }
+
+ device.Filter = "ip and tcp";
+ device.OnPacketArrival += DeviceOnOnPacketArrival;
+ device.StartCapture();
+ }
+ }
+
+ private void DeviceOnOnPacketArrival(object sender, PacketCapture e)
+ {
+ var rawPacket = e.GetPacket();
+
+ var packet = PacketDotNet.Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data);
+
+ var tcpPacket = packet.Extract<PacketDotNet.TcpPacket>();
+ if (tcpPacket is null)
+ {
+ return;
+ }
+
+ if (!tcpPacket.HasPayloadData || tcpPacket.PayloadData.Length == 0 || tcpPacket.PayloadData.Length > 500)
+ {
+ return;
+ }
+
+ var ipPacket = (PacketDotNet.IPPacket)tcpPacket.ParentPacket;
+ System.Net.IPAddress srcIp = ipPacket.SourceAddress;
+ System.Net.IPAddress dstIp = ipPacket.DestinationAddress;
+ int srcPort = tcpPacket.SourcePort;
+ int dstPort = tcpPacket.DestinationPort;
+
+ var tcpConnection = new TcpConnection(srcIp.Address, srcPort, dstIp.Address, dstPort);
+
+ if (!_connections.ContainsKey(tcpConnection))
+ {
+ _connections.TryAdd
+ (
+ tcpConnection,
+ new ConnectionData
+ (
+ srcIp,
+ srcPort,
+ dstIp,
+ dstPort,
+ new List<byte[]>(),
+ DateTimeOffset.Now
+ )
+ );
+ }
+
+ var data = _connections[tcpConnection];
+ if (data.SniffedData.Count < 5 && data.FirstObservedAt.AddSeconds(10) > DateTimeOffset.Now)
+ {
+ data.SniffedData.Add(tcpPacket.PayloadData);
+ }
+
+ if (_clients.TryGetValue(tcpConnection, out var client))
+ {
+ client.OnPacketArrival(tcpConnection, tcpPacket.PayloadData);
+ }
+ }
+
+ private async Task DeletionTask(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ foreach (var connectionData in _connections)
+ {
+ if (connectionData.Value.FirstObservedAt.AddMinutes(10) < DateTimeOffset.Now)
+ {
+ _connections.TryRemove(connectionData);
+ }
+
+ if (connectionData.Value.SniffedData.Count > 0 && connectionData.Value.FirstObservedAt.AddSeconds
+ (10) < DateTimeOffset.Now)
+ {
+ connectionData.Value.SniffedData.Clear();
+ }
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(30), ct);
+ }
+ catch (OperationCanceledException)
+ {
+ // ignored
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "The pcap manager deletion task has thrown an exception");
+ }
+ }
+ }
+}<
\ No newline at end of file
A Pcap/NosSmooth.Pcap/ProcessTcpManager.cs => Pcap/NosSmooth.Pcap/ProcessTcpManager.cs +113 -0
@@ 0,0 1,113 @@
+//
+// ProcessTcpManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+
+namespace NosSmooth.Pcap;
+
+/// <summary>
+/// A manager containing tcp connections, allowing notifications
+/// to <see cref="PcapNostaleClient"/> to know about any new connections.
+/// </summary>
+public class ProcessTcpManager
+{
+ private static TimeSpan RefreshInterval = TimeSpan.FromMilliseconds(9.99);
+
+ private readonly SemaphoreSlim _semaphore;
+ private readonly List<int> _processes;
+ private DateTimeOffset _lastRefresh;
+ private IReadOnlyDictionary<int, List<TcpConnection>> _connections;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProcessTcpManager"/> class.
+ /// </summary>
+ public ProcessTcpManager()
+ {
+ _lastRefresh = DateTimeOffset.MinValue;
+ _semaphore = new SemaphoreSlim(1, 1);
+ _processes = new List<int>();
+ _connections = new Dictionary<int, List<TcpConnection>>();
+ }
+
+ /// <summary>
+ /// Register the given process to refreshing list to allow calling <see cref="GetConnectionsAsync"/>
+ /// with that process.
+ /// </summary>
+ /// <param name="processId">The id of the process to register.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ public async Task RegisterProcess(int processId)
+ {
+ await _semaphore.WaitAsync();
+ try
+ {
+ _processes.Add(processId);
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ /// <summary>
+ /// Unregister the given process from refreshing list, <see cref="GetConnectionsAsync"/> won't
+ /// work for that process anymore.
+ /// </summary>
+ /// <param name="processId">The process to unregister.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ public async Task UnregisterProcess(int processId)
+ {
+ await _semaphore.WaitAsync();
+ try
+ {
+ _processes.Remove(processId);
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ /// <summary>
+ /// Get connections established by the given process.
+ /// </summary>
+ /// <remarks>
+ /// Works only for processes registered using <see cref="RegisterProcess"/>.
+ /// </remarks>
+ /// <param name="processId">The id of process to retrieve connections for.</param>
+ /// <returns>The list of process connections.</returns>
+ public async Task<IReadOnlyList<TcpConnection>> GetConnectionsAsync(int processId)
+ {
+ await Refresh();
+
+ if (!_connections.ContainsKey(processId))
+ {
+ return Array.Empty<TcpConnection>();
+ }
+
+ return _connections[processId];
+ }
+
+ private async Task Refresh()
+ {
+ if (_lastRefresh.Add(RefreshInterval) >= DateTimeOffset.Now)
+ {
+ return;
+ }
+
+ _lastRefresh = DateTimeOffset.Now;
+ if (_processes.Count == 0)
+ {
+ if (_connections.Count > 0)
+ {
+ _connections = new Dictionary<int, List<TcpConnection>>();
+ }
+ }
+
+ await _semaphore.WaitAsync();
+ _connections = TcpConnectionHelper.GetConnections(_processes);
+ _semaphore.Release();
+ }
+}<
\ No newline at end of file
A Pcap/NosSmooth.Pcap/TcpConnection.cs => Pcap/NosSmooth.Pcap/TcpConnection.cs +26 -0
@@ 0,0 1,26 @@
+//
+// TcpConnection.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace NosSmooth.Pcap;
+
+/// <summary>
+/// A tcp connection.
+/// </summary>
+/// <param name="LocalAddr">The local address.</param>
+/// <param name="LocalPort">The local port.</param>
+/// <param name="RemoteAddr">The remote address.</param>
+/// <param name="RemotePort">The remote port.</param>
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Fix this.")]
+public record struct TcpConnection
+(
+ long LocalAddr,
+ int LocalPort,
+ long RemoteAddr,
+ int RemotePort
+);<
\ No newline at end of file
A Pcap/NosSmooth.Pcap/TcpConnectionHelper.cs => Pcap/NosSmooth.Pcap/TcpConnectionHelper.cs +207 -0
@@ 0,0 1,207 @@
+//
+// TcpConnectionHelper.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
+
+namespace NosSmooth.Pcap;
+
+/// <summary>
+/// A class for obtaining process tcp connections.
+/// </summary>
+/// <remarks>
+/// Works on Windows only so far.
+/// </remarks>
+public static class TcpConnectionHelper
+{
+ private const int AF_INET = 2; // IP_v4 = System.Net.Sockets.AddressFamily.InterNetwork
+ private const int AF_INET6 = 23; // IP_v6 = System.Net.Sockets.AddressFamily.InterNetworkV6
+
+ /// <summary>
+ /// Get TCP IPv4 connections of the specified processes.
+ /// </summary>
+ /// <param name="processIds">The process ids to look for.</param>
+ /// <returns>Map from process ids to connecitons.</returns>
+ /// <exception cref="NotImplementedException">Thrown if not windows.</exception>
+ public static IReadOnlyDictionary<int, List<TcpConnection>> GetConnections(IReadOnlyList<int> processIds)
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ throw new NotImplementedException();
+ }
+
+ var result = new Dictionary<int, List<TcpConnection>>();
+ var tcpv4Connections = GetAllTCPv4Connections();
+
+ foreach (var connection in tcpv4Connections)
+ {
+ var process = processIds.FirstOrDefault(x => x == connection.OwningPid, -1);
+ if (process != -1)
+ {
+ if (!result.ContainsKey(process))
+ {
+ result.Add(process, new List<TcpConnection>());
+ }
+
+ result[process].Add
+ (
+ new TcpConnection
+ (
+ connection.LocalAddr,
+ (ushort)(connection.LocalPort[1] | (connection.LocalPort[0] << 8)),
+ connection.RemoteAddr,
+ (ushort)(connection.RemotePort[1] | (connection.RemotePort[0] << 8))
+ )
+ );
+ }
+ }
+
+ return result;
+ }
+
+ private static List<MIB_TCPROW_OWNER_PID> GetAllTCPv4Connections()
+ {
+ return GetTCPConnections<MIB_TCPROW_OWNER_PID, MIB_TCPTABLE_OWNER_PID>(AF_INET);
+ }
+
+ private static List<MIB_TCP6ROW_OWNER_PID> GetAllTCPv6Connections()
+ {
+ return GetTCPConnections<MIB_TCP6ROW_OWNER_PID, MIB_TCP6TABLE_OWNER_PID>(AF_INET6);
+ }
+
+ private static List<TIPR> GetTCPConnections<TIPR, TIPT>(int ipVersion)
+ {
+ // IPR = Row Type, IPT = Table Type
+
+ TIPR[] tableRows;
+ int buffSize = 0;
+ var dwNumEntriesField = typeof(TIPT).GetField("DwNumEntries");
+
+ // how much memory do we need?
+ uint ret = GetExtendedTcpTable
+ (
+ IntPtr.Zero,
+ ref buffSize,
+ true,
+ ipVersion,
+ TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL
+ );
+ IntPtr tcpTablePtr = Marshal.AllocHGlobal(buffSize);
+
+ try
+ {
+ ret = GetExtendedTcpTable
+ (
+ tcpTablePtr,
+ ref buffSize,
+ true,
+ ipVersion,
+ TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL
+ );
+ if (ret != 0)
+ {
+ return new List<TIPR>();
+ }
+
+ // get the number of entries in the table
+ TIPT table = (TIPT)Marshal.PtrToStructure(tcpTablePtr, typeof(TIPT))!;
+ int rowStructSize = Marshal.SizeOf(typeof(TIPR));
+ uint numEntries = (uint)dwNumEntriesField!.GetValue(table)!;
+
+ // buffer we will be returning
+ tableRows = new TIPR[numEntries];
+
+ IntPtr rowPtr = (IntPtr)((long)tcpTablePtr + 4);
+ for (int i = 0; i < numEntries; i++)
+ {
+ TIPR tcpRow = (TIPR)Marshal.PtrToStructure(rowPtr, typeof(TIPR))!;
+ tableRows[i] = tcpRow;
+ rowPtr = (IntPtr)((long)rowPtr + rowStructSize); // next entry
+ }
+ }
+ finally
+ {
+ // Free the Memory
+ Marshal.FreeHGlobal(tcpTablePtr);
+ }
+ return tableRows != null ? tableRows.ToList() : new List<TIPR>();
+ }
+
+ // https://msdn2.microsoft.com/en-us/library/aa366913.aspx
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MIB_TCPROW_OWNER_PID
+ {
+ public uint State;
+ public uint LocalAddr;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public byte[] LocalPort;
+ public uint RemoteAddr;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public byte[] RemotePort;
+ public uint OwningPid;
+ }
+
+ // https://msdn2.microsoft.com/en-us/library/aa366921.aspx
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MIB_TCPTABLE_OWNER_PID
+ {
+ public uint DwNumEntries;
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
+ public MIB_TCPROW_OWNER_PID[] Table;
+ }
+
+ // https://msdn.microsoft.com/en-us/library/aa366896
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MIB_TCP6ROW_OWNER_PID
+ {
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
+ public byte[] LocalAddr;
+ public uint LocalScopeId;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public byte[] LocalPort;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
+ public byte[] RemoteAddr;
+ public uint RemoteScopeId;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public byte[] RemotePort;
+ public uint State;
+ public uint OwningPid;
+ }
+
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa366905
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MIB_TCP6TABLE_OWNER_PID
+ {
+ public uint DwNumEntries;
+ [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
+ public MIB_TCP6ROW_OWNER_PID[] Table;
+ }
+
+ [DllImport("iphlpapi.dll", SetLastError = true)]
+ private static extern uint GetExtendedTcpTable
+ (
+ IntPtr pTcpTable,
+ ref int dwOutBufLen,
+ bool sort,
+ int ipVersion,
+ TCP_TABLE_CLASS tblClass,
+ uint reserved = 0
+ );
+
+ private enum TCP_TABLE_CLASS
+ {
+ TCP_TABLE_BASIC_LISTENER,
+ TCP_TABLE_BASIC_CONNECTIONS,
+ TCP_TABLE_BASIC_ALL,
+ TCP_TABLE_OWNER_PID_LISTENER,
+ TCP_TABLE_OWNER_PID_CONNECTIONS,
+ TCP_TABLE_OWNER_PID_ALL,
+ TCP_TABLE_OWNER_MODULE_LISTENER,
+ TCP_TABLE_OWNER_MODULE_CONNECTIONS,
+ TCP_TABLE_OWNER_MODULE_ALL
+ }
+}<
\ No newline at end of file