From ab84d2f44498c9563c164ed2ebe9bfe941b569d9 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 11 Feb 2023 17:33:37 +0100 Subject: [PATCH] feat(pcap): add tcp connection to pid helpers --- Pcap/NosSmooth.Pcap/ProcessTcpManager.cs | 113 +++++++++++ Pcap/NosSmooth.Pcap/TcpConnection.cs | 19 ++ Pcap/NosSmooth.Pcap/TcpConnectionHelper.cs | 207 +++++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 Pcap/NosSmooth.Pcap/ProcessTcpManager.cs create mode 100644 Pcap/NosSmooth.Pcap/TcpConnection.cs create mode 100644 Pcap/NosSmooth.Pcap/TcpConnectionHelper.cs diff --git a/Pcap/NosSmooth.Pcap/ProcessTcpManager.cs b/Pcap/NosSmooth.Pcap/ProcessTcpManager.cs new file mode 100644 index 0000000..7f24c01 --- /dev/null +++ b/Pcap/NosSmooth.Pcap/ProcessTcpManager.cs @@ -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; + +/// +/// A manager containing tcp connections, allowing notifications +/// to to know about any new connections. +/// +public class ProcessTcpManager +{ + private static TimeSpan RefreshInterval = TimeSpan.FromMilliseconds(0.99); + + private readonly SemaphoreSlim _semaphore; + private readonly List _processes; + private DateTimeOffset _lastRefresh; + private IReadOnlyDictionary> _connections; + + /// + /// Initializes a new instance of the class. + /// + public ProcessTcpManager() + { + _lastRefresh = DateTimeOffset.MinValue; + _semaphore = new SemaphoreSlim(1, 1); + _processes = new List(); + _connections = new Dictionary>(); + } + + /// + /// Register the given process to refreshing list to allow calling + /// with that process. + /// + /// The id of the process to register. + /// A representing the asynchronous operation. + public async Task RegisterProcess(int processId) + { + await _semaphore.WaitAsync(); + try + { + _processes.Add(processId); + } + finally + { + _semaphore.Release(); + } + } + + /// + /// Unregister the given process from refreshing list, won't + /// work for that process anymore. + /// + /// The process to unregister. + /// A representing the asynchronous operation. + public async Task UnregisterProcess(int processId) + { + await _semaphore.WaitAsync(); + try + { + _processes.Remove(processId); + } + finally + { + _semaphore.Release(); + } + } + + /// + /// Get connections established by the given process. + /// + /// + /// Works only for processes registered using . + /// + /// The id of process to retrieve connections for. + /// The list of process connections. + public async Task> GetConnectionsAsync(int processId) + { + await Refresh(); + + if (!_connections.ContainsKey(processId)) + { + return Array.Empty(); + } + + 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>(); + } + } + + await _semaphore.WaitAsync(); + _connections = TcpConnectionHelper.GetConnections(_processes); + _semaphore.Release(); + } +} \ No newline at end of file diff --git a/Pcap/NosSmooth.Pcap/TcpConnection.cs b/Pcap/NosSmooth.Pcap/TcpConnection.cs new file mode 100644 index 0000000..1f3414b --- /dev/null +++ b/Pcap/NosSmooth.Pcap/TcpConnection.cs @@ -0,0 +1,19 @@ +// +// 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; + +[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 diff --git a/Pcap/NosSmooth.Pcap/TcpConnectionHelper.cs b/Pcap/NosSmooth.Pcap/TcpConnectionHelper.cs new file mode 100644 index 0000000..fdec6d3 --- /dev/null +++ b/Pcap/NosSmooth.Pcap/TcpConnectionHelper.cs @@ -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; + +/// +/// A class for obtaining process tcp connections. +/// +/// +/// Works on Windows only so far. +/// +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 + + /// + /// Get TCP IPv4 connections of the specified processes. + /// + /// The process ids to look for. + /// Map from process ids to connecitons. + /// Thrown if not windows. + public static IReadOnlyDictionary> GetConnections(IReadOnlyList processIds) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new NotImplementedException(); + } + + var result = new Dictionary>(); + 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()); + } + + 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 GetAllTCPv4Connections() + { + return GetTCPConnections(AF_INET); + } + + private static List GetAllTCPv6Connections() + { + return GetTCPConnections(AF_INET6); + } + + private static List GetTCPConnections(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(); + } + + // 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(); + } + + // 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 -- 2.49.0