~ruther/NosSmooth

2b20fd6306aa33f001e2d977d70b701f992b9d74 — Rutherther 2 years ago 20e8f01 + d0d6eae
Merge pull request #74 from Rutherther/feat/pcap

Add support for pcap client
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

Do not follow this link