Mapping Sockets to a Process In .NET Code


One feature added to Fiddler a few years ago is the ability to map a given HTTP request back to the local process that initiated it. It turns out that this requires a bit of interesting code, because the .NET Framework itself doesn’t expose any built-in access to the relevant IPHelper APIs that provide this information.

I found a number of samples on the web, but for Fiddler, performance is a critical consideration because Fiddler needs to determine the originating process for every new connection. Hence, I’ve written the following code, which maximizes performance by minimizing copies between Windows and managed code.

// This sample is provided "AS IS" and confers no warranties.
// You are granted a non-exclusive, worldwide, royalty-free license to reproduce this code,
// prepare derivative works, and distribute it or any derivative works that you create.
//
// This class invokes the Windows IPHelper APIs that allow us to map sockets to processes.
// See http://www.pinvoke.net/default.aspx/iphlpapi/GetExtendedTcpTable.html as a reference
//
// We could consider a cache of recent hits to improve performance, but the performance is already pretty good, and
// creating a reasonable cache expiration policy could prove tricky. Client connection reuse already provides a significant
// optimization as it behaves in the same way as an explicit cache would.
//
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Net.NetworkInformation;
using System.Net;
using System.Diagnostics;
using System.Collections;

namespace Fiddler
{
internal class Winsock
{
#region IPHelper_PInvokes

private const int AF_INET = 2; // IPv4
private const int AF_INET6 = 23; // IPv6
private const int ERROR_INSUFFICIENT_BUFFER = 0x7a;
private const int NO_ERROR = 0x0;

// Learn about IPHelper here: http://msdn2.microsoft.com/en-us/library/aa366073.aspx and http://msdn2.microsoft.com/en-us/library/aa365928.aspx
// Note: C++'s ulong is ALWAYS 32bits, unlike C#'s ulong. See http://medo64.blogspot.com/2009/05/why-ulong-is-32-bit-even-on-64-bit.html
[DllImport("iphlpapi.dll", ExactSpelling = true, SetLastError = true)]
private static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref UInt32 dwTcpTableLength, [MarshalAs(UnmanagedType.Bool)] bool sort, UInt32 ipVersion, TcpTableType tcpTableType, UInt32 reserved);

/// <summary>
/// Enumeration of possible queries that can be issued using GetExtendedTcpTable
/// http://msdn2.microsoft.com/en-us/library/aa366386.aspx
/// </summary>
private enum TcpTableType
{
BasicListener,
BasicConnections,
BasicAll,
OwnerPidListener,
OwnerPidConnections,
OwnerPidAll,
OwnerModuleListener,
OwnerModuleConnections,
OwnerModuleAll
}

/* This code is now obsolete as I'm now using pointer-arithmetic to directly access the table rows instead of mapping structs on top of the
* returned block of data. I'm keeping the code here for now for debugging purposes.
// http://msdn2.microsoft.com/en-us/library/aa366913.aspx
[StructLayout(LayoutKind.Sequential)]
private struct TcpRow
{
[MarshalAs(UnmanagedType.U4)]
internal TcpState state;
[MarshalAs(UnmanagedType.U4)]
internal UInt32 localAddr;
[MarshalAs(UnmanagedType.U4)]
internal UInt32 localPortInNetworkOrder;
[MarshalAs(UnmanagedType.U4)]
internal UInt32 remoteAddr;
[MarshalAs(UnmanagedType.U4)]
internal UInt32 remotePortInNetworkOrder;
[MarshalAs(UnmanagedType.U4)]
internal Int32 owningPid;
}
private static string TcpRowToString(TcpRow rowInput)
{
return String.Format(">{0}:{1} to {2}:{3} is {4} by 0x{5:x}",
(rowInput.localAddr & 0xFF) + "." + ((rowInput.localAddr & 0xFF00) >> 8) + "." + ((rowInput.localAddr & 0xFF0000) >> 16) + "." + ((rowInput.localAddr & 0xFF000000) >> 24),
((rowInput.localPortInNetworkOrder & 0xFF00) >> 8) + ((rowInput.localPortInNetworkOrder & 0xFF) << 8),
(rowInput.remoteAddr & 0xFF) + "." + ((rowInput.remoteAddr & 0xFF00) >> 8) + "." + ((rowInput.remoteAddr & 0xFF0000) >> 16) + "." + ((rowInput.remoteAddr & 0xFF000000) >> 24),
((rowInput.remotePortInNetworkOrder & 0xFF00) >> 8) + ((rowInput.remotePortInNetworkOrder & 0xFF) << 8),
rowInput.state,
rowInput.owningPid);
}

*/
#endregion IPHelper_PInvokes

/// <summary>
/// Map a local port number to the originating process ID
/// </summary>
/// <param name="iPort">The local port number</param>
/// <returns>The originating process ID</returns>
internal static int MapLocalPortToProcessId(int iPort)
{
Debug.Assert(((iPort > 0) && (iPort < 65536)), "Unexpected client port value");
// Stopwatch oSW = Stopwatch.StartNew();
int result = FindPIDForPort(iPort);
// FiddlerApplication.Log.LogString("Port hunt took: " + oSW.ElapsedMilliseconds); // Current version seems to take about 1ms on average, with a range up to ~35ms.
return result;
}

/// <summary>
/// Calls the GetExtendedTcpTable function to map a port to a process ID.
/// This function is (over) optimized for performance.
/// </summary>
/// <param name="iTargetPort">Client port</param>
/// <param name="iAddressType">AF_INET or AF_INET6</param>
/// <returns>PID, if found, or 0</returns>
private static int FindPIDForConnection(int iTargetPort, uint iAddressType)
{
Debug.Assert(iAddressType == AF_INET6 || iAddressType == AF_INET);
IntPtr ptrTcpTable = IntPtr.Zero;
UInt32 tcpTableLength = 0;

int iOffsetToFirstPort = 12;
int iOffsetToPIDInRow = 12;
int iTableRowSize = 24; // 24 == Marshal.SizeOf(typeof(TcpRow));

// IPv6 tables are a different size, so adjust the offsets accordingly
if (iAddressType == AF_INET6)
{
iOffsetToFirstPort = 24;
iOffsetToPIDInRow = 32;
iTableRowSize = 56;
}

// Determine the size of the memory block to allocate
if (ERROR_INSUFFICIENT_BUFFER == GetExtendedTcpTable(ptrTcpTable, ref tcpTableLength, false, iAddressType, TcpTableType.OwnerPidConnections, 0))
{
try
{
ptrTcpTable = Marshal.AllocHGlobal((Int32)tcpTableLength);

// Would it be faster to set the SORTED argument to true, and then iterate the table in reverse order?
if (NO_ERROR == GetExtendedTcpTable(ptrTcpTable, ref tcpTableLength, false, iAddressType, TcpTableType.OwnerPidConnections, 0))
{
// Convert port we're looking for into Network byte order
int iTargetPortInNetOrder = ((iTargetPort & 0xFF) << 8) + ((iTargetPort & 0xFF00) >> 8);

// ISSUE: This function APPEARS to work fine, but might blow up on Itanium or exotic architectures like that. As noted in the docs:
// The MIB_TCPTABLE_OWNER_PID structure may contain padding for alignment between the dwNumEntries member and the first MIB_TCPROW_OWNER_PID
// array entry in the table member. Padding for alignment may also be present between the MIB_TCPROW_OWNER_PID array entries in the table member.
// Any access to a MIB_TCPROW_OWNER_PID array entry should assume padding may exist.
//
// I have absolutely no idea how to detect such padding, or if .NET handles it automatically if I use PtrToStructure rather than the direct pointer
// manipulation calls this function is now using.
//
int tableLen = Marshal.ReadInt32(ptrTcpTable); // Get table row count
if (tableLen == 0)
{
Debug.Assert(false, "How is it possible that the API succeeded and there are really no network connections? Maybe pure IPv6 environment?");
return 0;
}
IntPtr ptrRow = (IntPtr)((long)ptrTcpTable + iOffsetToFirstPort); // Advance pointer to first Port in the table

// Iterate each row of the table, looking to see if localPortInNetworkOrder matches. If it does, return the owningPid
for (int i = 0; i < tableLen; ++i)
{
// Check for matching local port
if (iTargetPortInNetOrder == Marshal.ReadInt32(ptrRow))
{
return Marshal.ReadInt32(ptrRow, iOffsetToPIDInRow);
// Note: the finally clause below will clean up memory
}

// Move to the next row
ptrRow = (IntPtr)((long)ptrRow + iTableRowSize);
}
}
else
{
FiddlerApplication.Log.LogFormat("GetExtendedTcpTable() returned error #{0}", Marshal.GetLastWin32Error().ToString());
return 0;
}
}
finally
{
// Clean up unmanaged memory block. Call succeeds even if tcpTable == 0.
Marshal.FreeHGlobal(ptrTcpTable);
}
}
else
{
FiddlerApplication.Log.LogFormat("Initial call to GetExtendedTcpTable() returned error #{0}", Marshal.GetLastWin32Error().ToString());
}
return 0;
}

/// <summary>
/// Given a local port number, uses GetExtendedTcpTable to find the originating process ID.
/// First checks the IPv4 connections, then looks at IPv6 connections
/// </summary>
/// <param name="iTargetPort">Client applications' port</param>
/// <returns>ProcessID, or 0 if not found</returns>
private static int FindPIDForPort(int iTargetPort)
{
int iPID = 0;
try
{
iPID = FindPIDForConnection(iTargetPort, AF_INET);
if ((iPID > 0) || !CONFIG.bEnableIPv6) return iPID;
return FindPIDForConnection(iTargetPort, AF_INET6);
}
catch (Exception eX)
{
FiddlerApplication.Log.LogFormat("Fiddler.Network.TCPTable> Unable to call IPHelperAPI function: {0}", eX.Message);
Debug.Assert(false, "Unable to call IPHelperAPI function" + eX.Message);
}

// If we got here, we didn't find the connection; this will occur if the connection is from a remote client.
// FiddlerApplication.Log.LogFormat("Fiddler.Network.TCPTable.Error> Unable to find process information for port #{0} in table of length {1}", iTargetPort, tcpTableLength);
return 0;
}
}
}
 
One caveat: the IPHelper APIs are only available on Windows XP or later, so before calling this code, you should verify that the platform supports it:
 
// Win2k Doesn't have iphlpapi.dll that we need, so disable Socket Mapping on that platform
if ((Environment.OSVersion.Version.Major < 6) && (Environment.OSVersion.Version.Minor < 1))
{
bMapSocketToProcess = false;
}

If the process ID returned is 0, then Fiddler was unable to determine what process created the socket. This might occur, for instance, if the request came from a non-local process running on another computer.
 
If a non-zero process ID for a connection is returned, you can use simple .NET methods to map the process ID to a process name:
 
try
{
System.Diagnostics.Process.GetProcessById(iPID).ProcessName.ToLower();
}
catch (Exception eX)
{
Debug.Assert(false, eX.Message);
}

It turns out that looking up a process name with the GetProcessById call can take quite a few milliseconds, and Process ID to Name mappings are fairly stable, so Fiddler maintains a cache of these mappings for 30 seconds.

 

I hope that you find this sample useful.
 
-Eric Lawrence
Comments (9)

  1. Invisible .NET application traffic says:

    Hi Eric,

    I have a weird issue with .NET applications using Fiddler.

    I am trying to make it go through Fiddler without success.

    I have reviewed the "Missing Traffic" topic, tried to modify the program.exe.config and machine.config without any success.

    The scenario is as follows:

    – A corporate environment, with a proxy that requires basic authentication.

    – I have added AuthN header in FiddlerScript so IE and other apps can use Fiddler without prompts.

    – I have winhttp set properly to use IE config after Fiddler initialization, so WinHTTP-based apps use Fiddler too.

    When Fiddler is capturing but WinHTTP is not set to use Fiddler, the .NET application does not perform web queries.

    When Fiddler is capturing (wether WinHTTP is set to use Fiddler or not), the .NET application DOES perform web queries and shows results, but Fiddler does not show any traces.

    To test this, you can use the "version check" of Reflector.NET.

    When using corporate proxy, it complains about proxy requesting authoriazation.

    When Fiddler is capturing, it says it is up to date, but no traffic is shown in Fiddler.

    Any hints?

  2. EricLaw [MSFT] says:

    @Invisible: Your best bet would be to post this question to the discussion group or email it to me, so I can ask questions like: "Is the traffic simply "invisible", in which case the problem's a filter" or is it actually not captured?

  3. EricLaw says:

    Note: There's a bug in the above code whereby the Windows GetExtendedTCPTable call fails. That can occur when connections are rapidly being opened and the size of the table changes in between the call to get the size of the table and the call to fill the table. To fix, you should probably call this function in a loop if you get a result of ERROR_INSUFFICIENT_BUFFER from the second call.

  4. stevieg says:

    I have to subtract 1 from the port number when this code to get the right process. Otherwise I end up with process 4, System. It doesn't seem like the right thing to do…

    stackoverflow.com/…/net-httplistener-how-to-identify-calling-process

  5. EricLaw says:

    The code shared above works just fine; it's used in Fiddler, which is used by millions of people.

    You should dump the entire port list and see what's going on; perhaps there's a problem in the value returned from context.Request.RemoteEndPoint.Port.

    I'm curious: is there some reason you're trying to build your own proxy server rather than using FiddlerCore?

  6. johnnystarr85@yahoo.com says:

    Hi Eric, I'm new to C#. I'm getting a complier error at the line that reads

    if ((iPID > 0) || !CONFIG.bEnableIPv6) return iPID;

    It reads "The Name CONFIG does not exist in the current context." I'm using VS2012 on OS MS Win 8.1 any Ideas how I can get this working.

    [EricLaw] Just remove the part in yellow. Inside Fiddler, CONFIG is a static class that has a Boolean that tracks whether IPv6 is allowed. Presumably, your app always allows IPv6, so you should delete this test.

  7. Avinash says:

    Hi Eric,

            I'm getting following exception while using selenium(firefox) and fiddler core.

    Process-determination failed. fiddler.config.path.lsof=/usr/sbin/lsof

    Call returned 2.

    System.ComponentModel.Win32Exception (0x80004005): The system cannot find the file specified

      at System.Diagnostics.Process.StartWithCreateProcess(ProcessStartInfo startInfo)

      at Fiddler.Winsock.GetPIDFromLSOF(Int32 iPort)

  8. EricLaw says:

    @Avinash: You didn't say what platform you're using, but the error message here suggests that you're using FiddlerCore on Mono on either Mac or Linux. The error message means exactly as it says– it can't find the lsof executable because the value of the fiddler.config.path.lsof preference is set incorrectly.

    The most likely explanation is that you could fix this by:

     FiddlerApplication.Prefs.SetStringPref("fiddler.config.path.lsof", "/usr/bin/lsof");

    But if that doesn't work, you'll need to find where lsof is on your system.

  9. EricLaw says:

    Note: Only the Mono builds of Fiddler use lsof because IPHelper.dll does not exist on Mac or Linux, and hence the code described in this blog post does not run on those platforms.