Skip to content

Commit

Permalink
WIP!
Browse files Browse the repository at this point in the history
refactor: Now using CsWin32 to generate P/Invoke methods and structures. Allows to use SafeHandles where available to limit memory leaks risk and improve stability. It also means a lot of files have been removed since they're now generated.
feat: chart's tooltip now show only one line per process (but it still has to be improved.
fix: GetMaxUserPort was reading the registry every single time, which is pretty unjustified. Caching it in a static variable to avoid this.
  • Loading branch information
wokhan committed Apr 26, 2023
1 parent a60bec0 commit 5df32a2
Show file tree
Hide file tree
Showing 64 changed files with 772 additions and 1,360 deletions.
10 changes: 10 additions & 0 deletions Common/Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
<Authors>Wokhan Solutions</Authors>
<OutputPath>..\bin\$(ConfigurationName)</OutputPath>
<Nullable>Enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<None Remove="**\NativeMethods.txt" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="**\NativeMethods.txt" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Wokhan.WindowsFirewallNotifier.Console.Tests" />
</ItemGroup>
Expand All @@ -28,6 +35,9 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.1" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.206-beta">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="System.Management" Version="7.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="7.0.0" />
Expand Down
1 change: 1 addition & 0 deletions Common/IO/Files/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
QueryDosDevice
13 changes: 0 additions & 13 deletions Common/IO/Files/PathResolver.NativeMethods.cs

This file was deleted.

5 changes: 4 additions & 1 deletion Common/IO/Files/PathResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

using Windows.Win32;

using Wokhan.WindowsFirewallNotifier.Common.Logging;

namespace Wokhan.WindowsFirewallNotifier.Common.IO.Files;
Expand Down Expand Up @@ -33,7 +36,7 @@ private static void InitDriveMapping()
foreach (var drive in drives)
{
trimmedDrive = drive.TrimEnd('\\');
if (NativeMethods.QueryDosDeviceW(trimmedDrive, sb, (uint)len) == 0)
if (NativeMethods.QueryDosDevice(trimmedDrive, sb, (uint)len) == 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Call to QueryDosDevice failed!");
}
Expand Down
7 changes: 7 additions & 0 deletions Common/NativeMethods.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"emitSingleFile": false,
"className": "NativeMethods",
"multiTargetingFriendlyAPIs": false,
"public": false
}
6 changes: 3 additions & 3 deletions Common/Net/IP/AF_INET.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

public abstract partial class IPHelper
{
internal enum AF_INET
internal class AF_INET
{
IP4 = 2,
IP6 = 23
internal const uint IP4 = 2;
internal const uint IP6 = 23;
}
}
6 changes: 0 additions & 6 deletions Common/Net/IP/BaseTcpTableOwnerModule.cs

This file was deleted.

241 changes: 241 additions & 0 deletions Common/Net/IP/Connection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
using System;
using System.ComponentModel;
using System.Net;
using System.Runtime.InteropServices;

using Windows.Win32;
using Windows.Win32.NetworkManagement.IpHelper;

using Wokhan.WindowsFirewallNotifier.Common.Logging;

namespace Wokhan.WindowsFirewallNotifier.Common.Net.IP;

public class Connection
{
private const uint NO_ERROR = 0;
private const uint ERROR_INSUFFICIENT_BUFFER = 122;
private const uint ERROR_NOT_FOUND = 1168;

public IPAddress RemoteAddress { get; private set; } = IPAddress.None;

public int RemotePort { get; private set; } = -1;

public IPAddress LocalAddress { get; private set; }

public int LocalPort { get; private set; }

public Owner? OwnerModule { get; private set; }

public string Protocol { get; init; } = "TCP";

public DateTime? CreationTime { get; init; }

public uint OwningPid { get; init; }

public ConnectionStatus State { get; private set; } = ConnectionStatus.NOT_APPLICABLE;

public bool IsLoopback { get; private set; }

private MIB_TCP6ROW tcp6MIBRow;

private IConnectionOwnerInfo sourceRow;

unsafe delegate uint GetOwnerModuleDelegate(object ROW, TCPIP_OWNER_MODULE_INFO_CLASS infoClass, void* buffer, ref int buffSize);

GetOwnerModuleDelegate getOwnerModule;

internal Connection(MIB_TCPROW_OWNER_MODULE tcpRow)
{
sourceRow = tcpRow;

OwningPid = tcpRow.dwOwningPid;
LocalAddress = new IPAddress(tcpRow.dwLocalAddr);
LocalPort = IPHelper.GetRealPort(tcpRow.dwLocalPort);
if (tcpRow.dwState != (uint)MIB_TCP_STATE.MIB_TCP_STATE_LISTEN)
{
RemoteAddress = new IPAddress(tcpRow.dwRemoteAddr);
RemotePort = IPHelper.GetRealPort(tcpRow.dwRemotePort);
}
OwnerModule = GetOwningModule();
State = (ConnectionStatus)tcpRow.dwState;
CreationTime = tcpRow.liCreateTimestamp == 0 ? null : DateTime.FromFileTime(tcpRow.liCreateTimestamp);
IsLoopback = IPAddress.IsLoopback(RemoteAddress);
}

internal Connection(MIB_TCP6ROW_OWNER_MODULE tcp6Row)
{
sourceRow = tcp6Row;

this.tcp6MIBRow = new MIB_TCP6ROW() { State = (MIB_TCP_STATE)tcp6Row.dwState, LocalAddr = new() { u = new() { Byte = tcp6Row.ucLocalAddr } }, RemoteAddr = new() { u = new() { Byte = tcp6Row.ucRemoteAddr } }, dwLocalPort = tcp6Row.dwLocalPort, dwRemotePort = tcp6Row.dwRemotePort };

OwningPid = tcp6Row.dwOwningPid;
LocalAddress = new IPAddress(tcp6Row.ucLocalAddr.AsSpan().ToArray());
LocalPort = IPHelper.GetRealPort(tcp6Row.dwLocalPort);
if (tcp6Row.dwState != (uint)MIB_TCP_STATE.MIB_TCP_STATE_LISTEN)
{
RemoteAddress = new IPAddress(tcp6Row.ucRemoteAddr.AsSpan().ToArray());
RemotePort = IPHelper.GetRealPort(tcp6Row.dwRemotePort);
}
OwnerModule = GetOwningModule();
State = (ConnectionStatus)tcp6Row.dwState;
CreationTime = tcp6Row.liCreateTimestamp == 0 ? null : DateTime.FromFileTime(tcp6Row.liCreateTimestamp);
IsLoopback = IPAddress.IsLoopback(RemoteAddress);
}

internal Connection(MIB_UDPROW_OWNER_MODULE udpRow)
{
sourceRow = udpRow;

OwningPid = udpRow.dwOwningPid;
LocalAddress = new IPAddress(udpRow.dwLocalAddr);
LocalPort = IPHelper.GetRealPort(udpRow.dwLocalPort);
OwnerModule = GetOwningModule();
Protocol = "UDP";
CreationTime = udpRow.liCreateTimestamp == 0 ? null : DateTime.FromFileTime(udpRow.liCreateTimestamp);
}

internal Connection(MIB_UDP6ROW_OWNER_MODULE udp6Row)
{
sourceRow = udp6Row;

OwningPid = udp6Row.dwOwningPid;
LocalAddress = new IPAddress(udp6Row.ucLocalAddr.AsSpan().ToArray());
LocalPort = IPHelper.GetRealPort(udp6Row.dwLocalPort);
OwnerModule = GetOwningModule();
Protocol = "UDP";
CreationTime = udp6Row.liCreateTimestamp == 0 ? null : DateTime.FromFileTime(udp6Row.liCreateTimestamp);
}

private bool EnsureStats(ref bool isAccessDenied)
{
if (Protocol != "TCP")
{
throw new InvalidOperationException("Statistics are not available for non-TCP connections. Please check first the connection's protocol.");
}

if (isAccessDenied || State != ConnectionStatus.ESTABLISHED || IPAddress.IsLoopback(RemoteAddress))
{
return false;
}

var result = new TCP_ESTATS_BANDWIDTH_RW_v0() { EnableCollectionInbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled, EnableCollectionOutbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled };
var r = sourceRow.SetPerTcpConnectionEStats(ref result, tcp6MIBRow);
if (r != 0)
{
throw new Win32Exception((int)r);
}

if (result.EnableCollectionInbound != TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled || result.EnableCollectionOutbound != TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled)
{
isAccessDenied = true;
return false;
}

return true;
}

ulong _lastInboundReadValue;
ulong _lastOutboundReadValue;

//TODO: not fond of those ref params, but using an interface prevents me to use local private fields - and using a property with a proper setter would result in a backing field creatino, breaking the initial struct layout.
public (ulong InboundBandwidth, ulong OutboundBandwidth) GetEstimatedBandwidth(ref bool isAccessDenied)
{
if (!EnsureStats(ref isAccessDenied))
{
_lastInboundReadValue = 0;
_lastOutboundReadValue = 0;

return (0, 0);
}

try
{
var rodObjectNullable = sourceRow.GetPerTcpConnectionEState(tcp6MIBRow);

if (rodObjectNullable is null)
{
isAccessDenied = true;
return (0, 0);
}

var rodObject = rodObjectNullable.Value;

// Fix according to https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-setpertcpconnectionestats
// One must subtract the previously read value to get the right one (as reenabling statistics doesn't work as before starting from Win 10 1709)
var inbound = rodObject.InboundBandwidth >= _lastInboundReadValue ? rodObject.InboundBandwidth - _lastInboundReadValue : rodObject.InboundBandwidth;
var outbound = rodObject.OutboundBandwidth >= _lastOutboundReadValue ? rodObject.OutboundBandwidth - _lastOutboundReadValue : rodObject.OutboundBandwidth;

_lastInboundReadValue = rodObject.InboundBandwidth;
_lastOutboundReadValue = rodObject.OutboundBandwidth;

return (inbound, outbound);
}
catch (Win32Exception we) when (we.NativeErrorCode == IPHelper.ERROR_NOT_FOUND)
{
_lastInboundReadValue = 0;
_lastOutboundReadValue = 0;

return (0, 0);
}
}


internal Owner? GetOwningModule()
{
if (OwningPid is 0 or 4)
{
return Owner.System;
}

Owner? ret = null;

var buffer = IntPtr.Zero;
try
{
// No need to set the proper size as it will be recomputed. See https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getownermodulefromtcp6entry#remarks
// Meaning that we cannont get the right size from the structure alone, we need to know the exact size including the resulting strings...
uint buffSize = 0;

var retn = sourceRow.GetOwnerModule(IntPtr.Zero, ref buffSize);
if (retn != NO_ERROR && retn != ERROR_INSUFFICIENT_BUFFER)
{
//Cannot get owning module for this connection
LogHelper.Info("Unable to get the connection owner: ownerPid=" + OwningPid + " remoteAdr=" + RemoteAddress + ":" + RemotePort);
return ret;
}

if (buffSize == 0)
{
//No buffer? Probably means we can't retrieve any information about this connection; skip it
LogHelper.Info("Unable to get the connection owner (no buffer).");
return ret;
}

buffer = Marshal.AllocHGlobal((int)buffSize);

var resp = sourceRow.GetOwnerModule(buffer, ref buffSize);
if (resp == NO_ERROR)
{
var ownerInfo = Marshal.PtrToStructure<TCPIP_OWNER_MODULE_BASIC_INFO>(buffer);
ret = new Owner(ownerInfo.pModuleName.ToString(), ownerInfo.pModulePath.ToString());
}
else if (resp != ERROR_NOT_FOUND) // Ignore closed connections
{
LogHelper.Error("Unable to get the connection owner.", new Win32Exception((int)resp));
}

return ret;
}
catch (Exception e)
{
return new Owner("ERROR", "");
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(buffer);
}
}
}
}
20 changes: 4 additions & 16 deletions Common/Net/IP/IConnectionOwnerInfo.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
using System;

using Wokhan.WindowsFirewallNotifier.Common.Net.IP.TCP;
using Windows.Win32.NetworkManagement.IpHelper;

namespace Wokhan.WindowsFirewallNotifier.Common.Net.IP;

public interface IConnectionOwnerInfo
{
// CHANGE
byte[] RemoteAddrBytes { get; }

string RemoteAddress { get; }
int RemotePort { get; }
string LocalAddress { get; }
int LocalPort { get; }
Owner? OwnerModule { get; }
string Protocol { get; }
DateTime? CreationTime { get; }
uint OwningPid { get; }
ConnectionStatus State { get; }
bool IsLoopback { get; }

ITcpRow ToTcpRow();
internal uint SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? tcp6Row);
internal TCP_ESTATS_BANDWIDTH_ROD_v0? GetPerTcpConnectionEState(MIB_TCP6ROW? tcp6Row);
internal uint GetOwnerModule(IntPtr buffer, ref uint buffSize);

}
11 changes: 0 additions & 11 deletions Common/Net/IP/IPHelper.NativeMethods.cs

This file was deleted.

Loading

0 comments on commit 5df32a2

Please sign in to comment.