Windows Containers Network isolation

· October 31, 2025

INTRO

There are a lot of the excellent documentations and researches about Windows containers:

https://www.cyberark.com/resources/threat-research-blog/understanding-windows-containers-communication
https://unit42.paloaltonetworks.com/what-i-learned-from-reverse-engineering-windows-containers/
https://googleprojectzero.blogspot.com/2021/04/who-contains-containers.html
https://qiita.com/kikuchi_kentaro/items/2fb0171e18821d402761
https://www.deepinstinct.com/blog/contain-yourself-staying-undetected-using-the-windows-container-isolation
https://blog.quarkslab.com/reversing-windows-container-episode-i-silo.html

But there is almost no information on how networking in Windows containers are implemented.

Let’s start our journey in Windows container networking:

Container networking on Windows

Windows containers function similarly to virtual machines in regards to networking. Each container has a virtual network adapter (vNIC) which is connected to a Hyper-V virtual switch (vSwitch). The Host Networking Service (HNS) and the Host Compute Service (HCS) work together to create containers and attach container vNICs to networks. HCS is responsible for the management of containers whereas HNS is responsible for the management of networking resources.

[ https://kubernetes.io/docs/concepts/services-networking/windows-networking/ ]

[ Containers networking ]

Host Networking Service (HNS)

The Windows Host Networking Service (HNS) is a backend component that manages networking for containers and virtual machines on a Windows host. Host Compute Network (HCN) service API is a public-facing Win32 API that provides platform-level access to manage the virtual networks, virtual network endpoints, and associated policies.

[ https://learn.microsoft.com/en-us/windows-server/networking/technologies/hcn/hcn-top ]

Microsoft samples show how to use Host Compute Network Service API to create a HCN on the host that can be used to connect Virtual NICS to Virtual Machines or Containers [hcn-scenarios]:

using unique_hcn_network = wil::unique_any<HCN_NETWORK,decltype(&HcnCloseNetwork),HcnCloseNetwork>;
/// Creates a simple HCN Network, waiting synchronously to finish the task
void CreateHcnNetwork()
{
    unique_hcn_network hcnnetwork;
    wil::unique_cotaskmem_string result;
    std::wstring settings = ....;
    GUID networkGuid;
    HRESULT result = CoCreateGuid(&networkGuid);
    result = HcnCreateNetwork(
        networkGuid,              // Unique ID
        settings.c_str(),      // Compute system settings document
        &hcnnetwork,
        &result
        );
    ....
}

All public HCN api can be categorized as:

  • Network : HcnCreateNetwork, HcnOpenNetwork, .etc
  • Namespace: HcnCreateNamespace, HcnOpenNamespace, .etc
  • Endpoint: HcnCreateEndpoint, HcnOpenEndpoint, .etc

What is a relation between Network, Namespace and Endpoint in terms of the HCN?

[ Network, Namespace and Endpoint relations ]

As we can see Namespace contains Network, and Network contains Endpoint

Each container endpoint is placed in its own network namespace. The management host virtual network adapter and host network stack are located in the default network namespace. To enforce network isolation between containers on the same host, a network namespace is created for each Windows Server container and containers run under Hyper-V isolation into which the network adapter for the container is installed. Windows Server containers use a host virtual network adapter to attach to the virtual switch. Hyper-V isolation uses a synthetic VM network adapter (not exposed to the utility VM) to attach to the virtual switch.

[ https://learn.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-isolation-security) ]


NAMESPACES

Searching for HcnCreateNamespace on Windows host reveals:

[ HcnCreateNamespace occurrence ]

Well, there are CmService.dll (Container Manager Service) and computenetwork.dll (Hyper-V Host Networking Service Library). And computenetwork.dll is our candidate:

[ HcnCreateNamespace occurence ]

Let’s open computenetwork.dll in IDA:

[ IDA pseudo code for HcnCreateNamespace in computenetwork.dll ]

HcnCreateNamespace in computenetwork.dll:

[ IDA pseudo code for HnsRpc_CreateNamespace in computenetwork.dll ]

So, here is the call chain HcnCreateNamespace -> HnsRpc_CreateNamespace. HnsRpc* prefix clearly says that creating namespaces will be performed in HostNetSvc.dll (Hns* - Host network service) playing the role of the RPC service, and it is an actual HNS.

Lets load HostNetSvc.dll into IDA:

[ HostNetSvc.dll Namespace-related methods ]

Namespace-related methods in the HostNetSvc.dll:

HNS::Service::Network::Namespace::Namespace(void)  
HNS::Service::Network::Namespace::~Namespace(void)  
HNS::Service::Network::Namespace::Teardown(void)  
HNS::Service::Network::Namespace::RemoveEndpointUnderLock(_GUID const &)  
HNS::Service::Network::Namespace::RemoveEndpoint(_GUID const &)  
HNS::Service::Network::Namespace::Modify(std::basic_string_view<ushort> const &)  
HNS::Service::Network::Namespace::Load(void)  
HNS::Service::Network::Namespace::IsHost(void)  
HNS::Service::Network::Namespace::DetachEndpoint(_GUID const &,ushort const *,bool)  
HNS::Service::Network::Namespace::Detach(wil::basic_zstring_view<ushort>,bool)  
HNS::Service::Network::Namespace::Create(void)  
HNS::Service::Network::Namespace::ContainersCount(void)  
HNS::Service::Network::Namespace::AttachHostEndpoint(_GUID const &)  
HNS::Service::Network::Namespace::AttachEndpoint(_GUID const &,ushort const *,bool)  
HNS::Service::Network::Namespace::AttachDefault(wil::basic_zstring_view<ushort>)  
HNS::Service::Network::Namespace::Attach(wil::basic_zstring_view<ushort>,bool)  
HNS::Service::Network::Namespace::AddEndpoint(_GUID const &)

Lets see at HNS::Service::Network::Namespace::Create method:

void __fastcall HNS::Service::Network::Namespace::Create(HNS::Service::Network::Namespace *this)  
{  
    ....  
    v8 = HNS::Service::Resource::ResourceManager::AllocateResource<25,HNS::Service::Resource::NetworkCompartmentResource::VariableSet *>( v6,v29,Activity,&v26);      
    ....  
}

Next go to Service::Resource::NetworkCompartmentResource:

HNS::Service::Resource::NetworkCompartmentResource::Load(void)  
HNS::Service::Resource::NetworkCompartmentResource::DeallocateResource(void)  
HNS::Service::Resource::NetworkCompartmentResource::AllocateResource(void)

HNS::Service::Resource::NetworkCompartmentResource::AllocateResource(void):

_QWORD *__fastcall HNS::Service::Resource::NetworkCompartmentResource::AllocateResource(__int64 a1, _QWORD *a2)  
{  
    .....  
    HNSTraceProvider::TraceEnter("HNS::Service::Resource::NetworkCompartmentResource::AllocateResource", 0x2Eu);  
    AcquireSRWLockExclusive((PSRWLOCK)(a1 + 40));  
    .....  
    v10 = IpAddress::CreateCompartment(&v15, v5 == 1, v4);  
    ....  
}

Here is IpAddress object, lets see:

IpAddress::GetCompartment(_GUID const &)  
IpAddress::DeleteCompartment(_GUID const &)  
IpAddress::CreateCompartment(_GUID const &,bool,bool)

And here is IpAddress::CreateCompartment method:

__int64 __fastcall IpAddress::CreateCompartment(const struct _GUID *a1, char a2, char a3)  
{  
    ....  
    CompartmentId = 0;  
    v6 = ConvertCompartmentGuidToId(a1, &CompartmentId);  
    ....  
    v13 = NsiSetAllParameters(2, 1, &NPI_MS_NDIS_MODULEID);  
    ....  
    return CompartmentId;  
}

So, Namespaces are implemented with the help of the Compartments!

We can find that the NsiSetAllParameters function is exported by netio.sys and nsi.dll:

[ Nsi.dll export ]



[ Netio.sys export ]

nsi.dll implements Network Store Interface (NSI). The NSI service is responsible for storing and providing information about network interfaces and their states to other parts of the operating system, and runs under a shared process called svchost.exe.

Let’s take a closer look at the NsiSetAllParameters function (IDA pseudo code):

__int64 __fastcall NsiSetAllParameters(int a1, int a2, LPVOID a3, int a4, LPVOID a5, int a6, LPVOID a7, int a8)  
{  
  _QWORD v9[3]; // [rsp+30h] [rbp-58h] BYREF  
  ....  
  return NsiIoctl(0x120013u, v9, 0x48u, v9, (LPDWORD)&a6, 0);  
}
DWORD __fastcall NsiIoctl(DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped)  
{  
    ULONG OutputBufferLength; // ebx  
    HANDLE EventA; // rdi  
    int Status; // ebx  
    HANDLE FileW; // rax  
    struct _IO_STATUS_BLOCK IoStatusBlock; // [rsp+58h] [rbp-30h] BYREF

    FileW = CreateFileW(L"\\\\.\\Nsi", 0, 3u, 0, 3u, 0x40000000u, 0);  
    ....
    if ( !lpOverlapped )  
    {  
        ....
        Status = NtDeviceIoControlFile(g_NsiAsyncDeviceHandle, EventA, 0, 0, &IoStatusBlock, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, OutputBufferLength);  
    }  
    ....  
}

Symbolic link L”\\.\Nsi” is created by nsiproxy.sys driver (discovered during nsiproxy.sys reversing):

__int64 __fastcall NsippCreateDevice(PDRIVER_OBJECT DriverObject)  
{  
    ....  
    RtlInitUnicodeString(&DestinationString, L"\\Device\\Nsi");  
    v8 = WdmlibIoCreateDeviceSecure(v1, v6, &DestinationString, v7, v14, v15, &DefaultSDDLString, v16);  
    if ( v8 >= 0 )  
    {  
        ....  
        RtlInitUnicodeString(&SymbolicLinkName, L"\\??\\Nsi");  
        v8 = IoCreateSymbolicLink(\&SymbolicLinkName, &DestinationString);  
        if ( v8 >= 0 )  
        {  
           *MajorFunction = (PDRIVER_DISPATCH)&NsippDispatch;  
    ....  
 }

NsippDispatch is a dispatch handler:

__int64 __fastcall NsippDispatch(__int64 a1, IRP *a2)  
{  
    ....  
    CurrentStackLocation = (__int64)a2->Tail.Overlay.CurrentStackLocation;  
    ....  
    v7 = *(_DWORD*)(CurrentStackLocation + 24);  
    v8 = *(volatile void **)(CurrentStackLocation + 32);  
    v10 = *(_DWORD *)(CurrentStackLocation + 16);  
    switch ( v7 )  
    {  
        ....  
      switch ( v7 )  
      {  
        ....  
        case 0x120013:  
          AllParameters = NsippSetAllParameters((__int128*)v8, v10, RequestorMode, CurrentStackLocation);  
    ....
}

And NsippSetAllParameters:

__int64 __fastcall NsippSetAllParameters(__int128* a1, unsigned int a2, unsigned __int8 a3, __int64 a4)  
{  
    ....   
    Parameters = NsippProbeAndAllocateParameters(  
                 (_DWORD)a1,  
                 72,  
                 (int)a1 + 8,  
                 (int)a1 + 40,  
                 (__int64)a1 + 56,  
                 a3,....);  
    v14 = (char*)Parameters;  
    ....  
LABEL_9:  
        Parameters = NsiSetAllParametersEx(v14);  
    ....  
}

And NsiSetAllParametersEx is located in netio.sys:

__int64 __fastcall NsiSetAllParametersEx(__int64 a1)  
{  
    ....  
    NmpContext = NsipGetNmpContext(a1 + 8);  
    ....  
    v11 = *(_QWORD*)(NmpContext + 48);  
    v5 = NsipValidateSetAllParametersRequest(v11, a1);  
    ....  
    v20 = NsipWritePersistentData(a1 + 8, v4, a1 + 40, *(_QWORD*)(a1 + 56), 0, *(_DWORD*)(a1 + 64), 0, v27);  
    ....
}

And NsipWritePersistentData function write all network related information into the registry key L”\Registry\Machine\System\CurrentControlSet\Control\Nsi”:

__int64 __fastcall NsipWritePersistentData(  
        __int64 a1,  
        unsigned int a2,  
        __int64 a3,  
        void *a4,  
        void *Src,  
        unsigned int Size,  
        unsigned int a7,  
        bool a8)  
{  
    ....  
    result = NsipOpenInformationObjectKey(&KeyHandle);  
    ....  
    v16 = ZwQueryValueKey(KeyHandle, &ValueName, KeyValueFullInformation, v33, DataSize, &DataSize);  
    ....  
    NsipConvertKeyValueDataToRw(LowPriority, LowPriority, 0, 0, v35);  
    ....  
    v24 = ZwSetValueKey(KeyHandle, &ValueName, 0, 3u, v12, 2 * v20);  
    ....
}  
    
__int64 __fastcall NsipOpenInformationObjectKey(PHANDLE KeyHandle, unsigned int *a2)  
{  
  ....  
  PersistedStateLocation = RtlGetPersistedStateLocation(  
                                 L"NsiPersistentStore",  
                                 0,  
                                 L"\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nsi",  
                                 0,  
                                 v17,  
                                 512,  
                                 &v11);  
.... 
}


View of the \Registry\Machine\System\CurrentControlSet\Control\Nsi:

[ NSI tables in a registry ]

Full call chain for namespaces creation:

[ NSI call chain ]


COMPARTMENTS

So, to create Namespace (internally implemented via Compartments) we can use Hcn* functions or Nsi* functions.
A traffic from the one Namespace ( Compartment ) can’t get to the others by default.
Let’s search for something compartment-related:

[ Searching for ‘CreateCompartment’ ]

Container.dll sounds promising! There are a lot of the interesting things:

[ Inside container.dll]

NetworkProvider::SetupCompartment method:

__int64 __fastcall container::NetworkProvider::SetupCompartment(_QWORD *a1, container_runtime *a2)  
{  
    ....  
    memset_0(v9, 0, 0x21Cu);  
    InitializeCompartmentEntry(v9);  
    container_runtime::GetContainerIdentifier(a2, v9, 0, 0, *(unsigned int **)v7);  
    if ( a1[3] > 7u )  
        a1 = (_QWORD *)*a1;  
    _o_wcscpy_s(v10, 257, a1);  
    v11 |= 1u;  
    Compartment = CreateCompartment(v9);  
 
}

Functions InitializeCompartmentEntry and CreateCompartment are imported from IPHLPAPI.DLL:

[ Container.dll imported functions ]

Firstly look at InitializeCompartmentEntry function:

__int64 __fastcall InitializeCompartmentEntry(__int64 a1)  
{  
    __int64 result; // rax  
    memset_0((void *)a1, 255, 0x21Cu);  
    result = 0;  
    *(_DWORD *)(a1 + 16) = 0;  
    *(_WORD *)(a1 + 20) = 0;  
  return result;  
}

It provides information about compartment entry structure, it has size of the 0x21C bytes and at least two fields one with _DWORD type at 0x10 bytes offset, and other with _WORD type at 0x14 bytes.

Next look at CreateCompartment function:

__int64 __fastcall CreateCompartment(const wchar_t *a1)  
{  
    _DWORD v4[4]; // [rsp+58h] [rbp-B0h] BYREF  
    _DWORD v5[270]; // [rsp+68h] [rbp-A0h] BYREF  
    wchar_t pszDest[259]; // [rsp+4B2h] [rbp+3AAh] BYREF  
    int v9; // [rsp+6B8h] [rbp+5B0h]  
    v4[0] = 0;  
    memset_0(v5, 0, 0x668u);  
    v5[0] = 107479981;  
    .... 
    LODWORD(result) = StringCbCopyW(pszDest, 0x202u, a1 + 10);  
    ....
    if ( (a1[268] & 1) != 0 )  
        v9 |= 4u;  
    result = NsiSetAllParameters(1, 1, &NPI_MS_NDIS_MODULEID, 7, v4, 4, v5, 1640);
    ....
}

So, it is obvious that input for CreateCompartment is some structure.
After some manipulations we can reconstruct compartment entry structure:

typedef struct _COMPARTMENT_ENRTY  
{  
    GUID    Guid;                
    DWORD   CompartmentId;  
    WORD    DescriptionSize;  
    WCHAR   Description[257];  
    DWORD   Flags;  
    // Structure size: 0x21C (540 bytes)  
} COMPARTMENT_ENRTY, *PCOMPARTMENT_ENRTY;

Now we can create our own compartment initializing COMPARTMENT_ENRTY and invoking CreateCompartment from the IPHLPAPI.DLL:

[ Create ‘MyCustom Compartment’ with generated GUID ]



[ Newly created ‘MyCustom Compartment’ ]

We already know that the root of the isolation is the compartment (namespace) mechanism.
So obviously modules involved in a network stack somehow have to know about the compartment.

For user mode we have ap exported by IPHLPAPI.DLL like:

GetCurrentThreadCompartmentId  
GetSessionCompartmentId  
GetCurrentThreadCompartmentScope  
GetJobCompartmentId  
GetInterfaceCompartmentId

SetCurrentThreadCompartmentId  
SetSessionCompartmentId  
SetCurrentThreadCompartmentScope  
SetJobCompartmentId  
SetInterfaceCompartmentId

For kernel mode we have api exported by ndis.sys like:

NdisGetThreadObjectCompartmentId  
NdisGetSessionCompartmentId  
NdisGetProcessObjectCompartmentId  
NdisGetJobObjectCompartmentId  
NdisGetThreadObjectCompartmentScope

NdisSetThreadObjectCompartmentId  
NdisSetSessionCompartmentId  
NdisSetProcessObjectCompartmentId  
NdisSetJobObjectCompartmentId  
NdisSetThreadObjectCompartmentScope

So modules involved in a network stack can just call this api to get a compartment and perform appropriate actions regarding compartment information. It is called ‘module is compartment-aware’. And if we do search for this api on disk C: we see that it is true:

[ Kernel mode compartment-aware modules ]



[ User mode compartment-aware modules ]

For example let’s see at exported from netio.sys function GetIpPath:

[ GetIpPath function from netio.sys ]

Here is reconstructed code for GetIpPath function, that fill ip path based on compartment:

NTSTATUS __stdcall GetIpPathTable(ADDRESS_FAMILY Family,  PMIB_IPPATH_TABLE *Table)  
{  
    ....
    ULONG CurrentCompartmentId = GetCurrentThreadCompartmentId();  
    ....
    // Get path table for each family  
    for (FamilyIndex = 0; FamilyIndex < 2; FamilyIndex++) {  
        GetModuleIdFromFamily(FamilyTable[FamilyIndex], &ModuleId);          
        // Allocate and retrieve NSI path table  
        NTSTATUS Status = NsiAllocateAndGetTable(&ModuleId, .... pathPtr, keyPtr, rodPtr,  
                                                    &FamilyData[FamilyIndex].NumEntries);         
        // Count entries matching current compartment  
        ....
        for (DWORD i = 0; i < FamilyData[FamilyIndex].NumEntries; i++) {  
                PVOID keyEntry = (PBYTE)pathBase + (i * keySize);  
                if (*(PULONG)keyEntry == CurrentCompartmentId) {  
                    TotalEntries++;  
                }  
            }  
        }  
    }      
    // Allocate output MIB table  
    Status = NetioAllocateMibTable(TotalEntries, MIB_IPPATH_TABLE_VERSION, &PathTable);      
    for (FamilyIndex = 0; FamilyIndex < 2; FamilyIndex++) {   
        for (i = 0; i < FamilyData[FamilyIndex].NumEntries; i++) {  
            ....
            if (*(PULONG)keyEntry == CurrentCompartmentId) {  
                // Fill MIB_IPPATH_ROW entry  
                FillIpPathEntry( (PMIB_IPPATH_ROW)((PBYTE)PathTable + .....);  
                OutputIndex++;  
            }  
        }  
    }  
    *Table = PathTable;
    ....
}


KERNEL

We already know that CreateCompartment from the IPHLPAPI.DLL leads to the netio.sys -> NsiSetAllParametersEx

[ NsiSetAllParametersEx from the netio.sys ]

Analyzing NsiSetAllParametersEx function we can see that it gets a pointer to the some network manager provider and calls the handler for SetAllParameters from this provider:

v11 = 104LL * *(unsigned int *)(a1 + 24);  
v16[1] = *(_QWORD *)(v11 + *(_QWORD *)(v8 + 8) + 24);  
v2 = (*(__int64 (__fastcall **)(_QWORD *))(v11 + *(_QWORD *)(v8 + 8) + 56))(v16);

Let’s run a Windbg kernel debug session, install a break point on the netio!NsiSetAllParametersEx and run our console application that creates a compartment.

After we ran the application stopped at the netio!NsiSetAllParametersEx in the Windbg, and during indirect call through CFG (Control Flow Guard) we landed at ndis!ndisSetAllCompartment function:

[ Indirect call through CFG to the ndis!ndisSetAllCompartments ]



[ Call stack for ndis!ndisSetAllCompartments ]


NDIS

NDIS stands for Network Driver Interface Specification.
It is the core networking framework in Windows — a kernel subsystem and API standard that defines how network interface drivers interact with the operating system and each other:

[ NDIS stack ]

Let’s analyze ndis.sys for any compartment-aware functionality:

[ Compartment-aware functions in ndis.sys ]

Functionality of the ndisSetAllCompartments:

[ ndisSetAllCompartments function ]

So, ndisSetAllCompartments based on input parameters can create or delete the compartment (ndisIfCreateCompartment/ndisIfDeleteCompartment).

Let’s see what ndisIfCreateCompartment does:

[ ndisIfCreateCompartment function ]

Setup namespace guid as provided compartment guid:

[ Setup LoopbackInfo inside ndisIfCreateCompartment ]

LoopbackIfNetworkGuid and CompartmentId get set up in the ndisIfCreateCompartmentBlock function:

[ ndisIfCreateCompartmentBlock function ]

Network gets created with guid from the generated LoopbackIfNetworkGuid:

[ Network creation in the ndisIfCreateCompartment function ]

Loopback interface creation:

[ Loopback interface creation in the ndisIfCreateCompartment function ]

What we can see here is that for each compartment Loopback interface must be created (obviously to not reach host Loopback from the Namespaces).

Compartments creation call chain:

[ Call chain for compartment creation ]

Objects links involved to the compartment creation :

[ ndis.sys objects links ]

Debugging our compartment creation we can observe that “Software Loopback Interface 2” with InterfaceGuid {a3470f2f-b255-11f0-921d-000c29bc37c7} gets created and attached to newly created Network with NetworkGuid {a3470f2e-b255-11f0-921d-000c29bc37c7}:

[ "Software Loopback Interface 2" ]

Call stack for compartment creation from Windbg:

kd> k  
fffff987`d3cc2dff fffff800`6c96a460   ndis!ndisIfRegisterInterfaceEx  
fffff987`d3cc2dff fffff800`6c915447   ndis!ndisIfCreateInterface  
fffff987`d3cc2d98 fffff800`6c915246   ndis!ndisIfCreateNetworkBlock  
fffff987`d3cc2da0 fffff800`6ccf41b9   ndis!ndisNsiSetAllNetworkInfo+0x356   
fffff987`d3cc3080 fffff800`6c987ad1   NETIO!NsiSetAllParametersEx+0x139  
fffff987`d3cc3160 fffff800`6c9128a5   ndis!ndisIfCreateNetwork+0xf9  
fffff987`d3cc3430 fffff800`6c91326d   ndis!ndisIfCreateCompartment+0x1ed  
fffff987`d3cc34b0 fffff800`6ccf41b9   ndis!ndisNsiSetAllCompartment+0xad  
fffff987`d3cc3500 fffff800`6fd91994   NETIO!NsiSetAllParametersEx+0x139  
fffff987`d3cc35e0 fffff800`6fd92860   nsiproxy!NsippSetAllParameters+0x1f4  
fffff987`d3cc37a0 fffff800`676d21c5   nsiproxy!NsippDispatch+0x200  
fffff987`d3cc37f0 fffff800`67a4c801   nt!IofCallDriver+0x55  
fffff987`d3cc3830 fffff800`67a4c43a   nt!IopSynchronousServiceTail+0x361  
fffff987`d3cc38d0 fffff800`67a4b716   nt!IopXxxControlFile+0xd0a  
fffff987`d3cc3a20 fffff800`67811508   nt!NtDeviceIoControlFile+0x56  
fffff987`d3cc3a90 00007ffd`abe2d5d4   nt!KiSystemServiceCopyEnd+0x28  
0000006d`e439e688 00007ffd`ab3d216a   ntdll!NtDeviceIoControlFile+0x14  
0000006d`e439e690 00000000`00000000   0x00007ffd`ab3d216a

So, during compartment creation following objects are set up:

  • _NDIS_IF_COMPARTMENT_BLOCK;
  • _NDIS_IF_NETWORK_BLOCK, linked to _NDIS_IF_COMPARTMENT_BLOCK;
  • _NDIS_IF_BLOCK for Loopback Interface, linked to _NDIS_IF_NETWORK_BLOCK;

Obviously our compartment is almost empty, it doesn’t contain any interfaces that point to the real or virtual adapters, only the loopback interface. We should somehow add a working interface to our compartment. But to add an interface firstly we should create it.

How is it linked to a real (or virtual) communication channel?
Microsoft says that a miniport driver communicates with its NICs (network interface card) and with higher-level drivers through the NDIS library.

[ https://learn.microsoft.com/en-us/windows-hardware/drivers/network/windows-network-architecture-and-the-osi ]

So the underlying component for an interface is miniport (real or virtual). An interface is linked to a miniport.


INTERFACE

According to Microsoft documentation a network interface is the point where two pieces of network equipment or protocol layers connect. Network interfaces are defined by the Internet Engineering Task Force (IETF) in RFC 2863 ([ RFC 2863 ]). An interface is just an abstraction for underlying components.

[ https://learn.microsoft.com/en-us/windows/win32/network-interfaces ]

In the ndis.sys interface is presented as _NDIS_IF_BLOCK:

_NDIS_IF_BLOCK  
ifIndex         	: Uint4B  
ifDescr         	: _IF_COUNTED_STRING_LH  
ifType           	: Uint2B  
InterfaceGuid   	: _GUID  
CompartmentId   	: Uint4B  
Compartment     	: Ptr64 _NDIS_IF_COMPARTMENT_BLOCK  
NetLuid         	: _NET_LUID_LH  
NetworkGuid     	: _GUID  
NetworkLink     	: _LIST_ENTRY  
Network         	: Ptr64 _NDIS_IF_NETWORK_BLOCK  
MiniportAvailable 	: UChar  
Miniport         	: Ptr64 _NDIS_MINIPORT_BLOCK  
Filter           	: Ptr64 _NDIS_FILTER_BLOCK

_NDIS_IF_BLOCK get created by the ndisIfCreateInterface with help of the ndisIfRegisterInterfaceEx function:

[ References to the ndisIfCreateInterface function ]

ndisIfRegisterInterfaceEx performs registration of the newly created _NDIS_IF_BLOCK into the NDIS stack:

  • allocates interface index (ndisIfAllocateIfIndex);
  • links interface to the corresponding network (ndisIfFindNetworkBlock);
  • link compartment to the interface (CompartmentId and Compartment fields);
  • notifies NSI clients (ndisNsiNotifyClientInterfaceChange);

We have already analyzed ndisIfCreateCompartment function, which actually creates only loopback interface:

[ Loopback interface creation inside ndisIfCreateCompartment ]

Interfaces for loopback get also created during NDIS initialization in ndisIfCompartmentSubsystemInitializePhase3 function:

[ Loopback interface creation inside ndisIfCompartmentSubsystemInitializePhase3 ]

Interface for lightweight filter (LWF - [ NDIS LWF ]) gets created in ndisIfCreateFilterInterface function:

[ LWF interface creation inside ndisIfCreateFilterInterface ]

Interface for usual network setup installation [ network setup installation ] gets created in ndisIfCreateInterfaceFromPersistentStore function:

[ NetSetup interface creation inside ndisIfCreateInterfaceFromPersistentStore ]

Interfaces created by third-party components just get registered via exported from the ndis.sys NdisIfRegisterInterface as:

[ Exported NdisIfRegisterInterface function for third-party components ]

For instance wfplwfs.sys (WFP NDIS LWF Driver implementing WFP subsystem) use NdisIfRegisterInterface inside vSwitchFlpCreateAdapter function:

[ Call to NdisIfRegisterInterface in wfplwfs.sys ]

The remaining part here is to find the place where the interface gets attached to a miniport.


MINIPORT

According to Microsoft documentation a miniport driver communicates with its NICs and with higher-level drivers through the NDIS library. The NDIS library exports a full set of functions (NdisMXxx and other NdisXxx functions) that encapsulate all of the operating system functions that a miniport driver must call.

An NDIS miniport driver has two basic functions:

  • Managing a network interface card, including sending and receiving data through this interface;
  • Interfacing with higher-level drivers, such as filter drivers, intermediate drivers, and protocol drivers;

In ndis.sys miniport is presented as _NDIS_MINIPORT_BLOCK:

_NDIS_MINIPORT_BLOCK  
InterfaceGuid    	: _GUID  
NetLuid          	: _NET_LUID_LH  
IfBlockAvailable 	: UChar  
IfBlock          	: Ptr64 _NDIS_IF_BLOCK  
IfIndex         	: Uint4B

Miniport is created in the ndisAddDevice function:

[ Miniport creation in the ndisAddDevice function ]

ndisAddDevice gets invoked from the two places, ndisPnPAddDevice and ndisLWMCreateMiniport functions.

ndisLWMCreateMiniport is used by NetAdapterCx ([ netcx ]), ndisPnPAddDevice gets invoked during devices (real or virtual) detection by PnP manager ([ state-transitions-for-pnp-devices ]).

Let’s analyze ndisAddDevice function:

[ ndisAddDevice function ]

NDIS_ADDDEVICE_PARAMETERS is filled in the ndisPnPAddDevice function before invoking ndisAddDevice:

NDIS_ADDDEVICE_PARAMETERS  
InterfaceGuid		: _GUID  
NetLuid         	: _NET_LUID_LH  
HideInUi         	: Bool  
MiniBlock        	: Ptr64 _NDIS_M_DRIVER_BLOCK

ndisPnPAddDevice reads information about the interface from the registry key HKLM\SYSTEM\CurrentControlSet \Control\Class{4d36e972-e325-11ce-bfc1-08002be10318}{devId} and also interface guid (HKLM\SYSTEM\CurrentControlSet\Control\Class{4d36e972-e325-11ce-bfc1-08002be10318}{devId}\NetCfgInstanceId), this information gets into registry during network component installation.

ndisAddDevice basing on the provided NDIS_ADDDEVICE_PARAMETERS finds corresponding interface in the ndisIfList (ndisIfFindInterfaceByInterfaceGuid), and updates this interface with help of ndisIfUpdateInterfaceOnAddDevice:

[ ndisAddDevice function ]

ndisIfUpdateInterfaceOnAddDevice function:

  • links miniport (the MiniBlock field of the provided NDIS_ADDDEVICE_PARAMETERS) to interface and interface to miniport;
  • sets IfBlockAvailable field of the _NDIS_MINIPORT_BLOCK and MiniportAvailable field of the _NDIS_IF_BLOCK;
  • update NSI database if it is needed (ndisIfUpdatePersistedInterfaceInfo):

[ ndisIfUpdateInterfaceOnAddDevice function ]

There is also ndisPnpRefresh function, but it is not invoked by PnP manager, it gets triggered by other network components, directly via IOCTL (code 0x1700CC) to the “Device\Ndis”.

ndisPnpRefresh triggers ndisIfCreateOrUpdateInterface function:

[ ndisIfCreateOrUpdateInterface function ]

So, it is pretty clear what ndisIfCreateOrUpdateInterface does, it opens interface persisted storage, retrieves persisted state objects for some interface by its guid, and based on this information creates (ndisLoadNetworkInterfaceFromPersistedState) or updates (ndisIfUpdateIfBlockFromPersistedState) specified interface.

Let’s see what ndisLoadNetworkInterfaceFromPersistedState does:

[ ndisLoadNetworkInterfaceFromPersistedState function ]

ndisLoadNetworkInterfaceFromPersistedState create interface from the persisted state with help of the ndisIfCreateInterfaceFromPersistentStore (we already know about this function):

[ ndisIfCreateInterfaceFromPersistentStore function ]

ndisIfCreateInterfaceFromPersistentStore flow:

[ ndisIfCreateInterfaceFromPersistentStore flow ]

From the flow above we can see that the interface will go to the default compartment (with ID = 1) if there is no specified network or compartment for it!

Let’s see what ndisIfUpdateIfBlockFromPersistedState does:

[ ndisIfUpdateIfBlockFromPersistedState function ]

ndisIfReadNetworkGuidFromKey function:

[ ndisIfReadNetworkGuidFromKey function ]

Here we can see if ndisIfReadNetworkGuidFromKey can’t find compartment guid from the persisted state object as well as network guid it returns default network guid (default network belongs to default compartment) for the updated interface.

So every interface will go to the default compartment if there is no specified network or compartment for it!

[ Defult compartment (ID = 1) ]

After that ndisIfUpdateIfBlockFromPersistedState tries to invoke ndisIfUpdateInterfaceIsolationNetworkId:

[ ndisIfUpdateInterfaceIsolationNetworkIdLocked function ]

This function actually changes which network (by provided GUID) an interface belongs to, if it is needed!

ndisIfUpdateInterfaceIsolationNetworkIdLocked is also used in the ndisNsiChangeInterfaceInfo triggered by the ndisNsiSetInterfaceInformation function.

Now everything is linked, miniport <-> interface <-> network <-> compartment!

Let’s summarise:

COMPARTMENT  
 ├─ NETWORK (NDIS_IF_NETWORK_BLOCK)  
     ├─ INTERFACE(S) (NDIS_IF_BLOCK)  
         └─ MINIPORT (NDIS_MINIPORT_BLOCK)  
              └─ ADAPTER / DRIVER  
     └─ LoopbackIf (mandatory)  
 └─ Other networks
  • we created compartment with network and loopback interface, linked it together (CreateCompartmen -> ndisIfCreateCompartment);
  • newly created compartment contains only interface for Loopback;
  • all interfaces created during network components discovering are automatically attached to the default compartment (ndisIfFindCompartmentBlock(1));
  • interface can be attached to another compartment using ndisIfUpdateInterfaceIsolationNetworkId;


NET SETUP

All information about created interfaces, networks, compartments and binding between them is stored in the \Registry\Machine\System\CurrentControlSet\Control\NetworkSetup2:

[ Access denied to the NetworkSetup2 ]

But we can run regedit.exe via [ RunAsTI64.exe ]:

[ View of the NetworkSetup2 ]



[ NetSetup objects types ]

Path to the NetworkSetup2 registry is constructed by netsetupBuildObjectPath from the ndis.sys:

[ netsetupBuildObjectPath function signature ]



[ NetSetup subkeys types ]



[ netsetupBuildObjectPath registry path creation ]



[ Path in the Network2 registry ]

This registry location is used during interfaces creation (NdisIfBlockSourcePersistedNetSetup). In user mode NetSetup is implemented by NetSetupApi.dll, NetMgmtIF.dll and netcfgx.dll, setupapi.dll. When the new network component is going to be installed (for instance from the INF file) NetSetup does this work.

NetMgmtIF.dll is aware about compartments:

[ NetMgmtIF.dll compartment-related functions ]

MOVE INTERFACE TO THE COMPARTMENT

Time to move some actual interface to our compartment!
To experiment with moving interfaces to the compartments additional adapter [-> interface] (perfectly with internet access) should be added to a tested system.
As we already know interface can be moved to another compartment using ndisIfUpdateInterfaceIsolationNetworkId function:

[ ndisIfUpdateInterfaceIsolationNetworkId functions ]

Signature for ndisIfUpdateInterfaceIsolationNetworkId:

__int64 __fastcall ndisIfUpdateInterfaceIsolationNetworkId  
(  
        struct _NDIS_IF_BLOCK *ifaceBlock,  
        const struct _GUID *networkGuid,  
        char flags  
);

ifaceBlock - interface that is going to be moved to another compartment;
networkGuid - guid of the network that belongs to the another compartment;

How to get networkGuid and ifaceBlock?
There is some kind of the ‘helper’ function NdisMSetInterfaceCompartment:

[ NdisMSetInterfaceCompartment function ]

Signature for NdisMSetInterfaceCompartment:

__int64 __fastcall NdisMSetInterfaceCompartment  
(  
        _NDIS_MINIPORT_BLOCK *miniportBlock,  
        const struct _GUID *compartmentGuid  
);  

miniportBlock - miniport that has attached interface which is going to be moved;
compartmentGuid - guid of the compartment we want to move interface to;

Obviously we have to somehow get required _NDIS_MINIPORT_BLOCK.

One of the ways to obtain _NDIS_MINIPORT_BLOCK is to create a NDIS lightweight filter and in the FILTER_ATTACH_HANDLER reach _NDIS_MINIPORT_BLOCK.

[ ndis-filter-attach ] :

NDIS_STATUS  
FilterAttach(  
    IN  NDIS_HANDLE                     NdisFilterHandle,  
    IN  NDIS_HANDLE                     FilterDriverContext,  
    IN  PNDIS_FILTER_ATTACH_PARAMETERS  AttachParameters  
    );

Ndis.sys will call our FILTER_ATTACH_HANDLER in the ndisFInvokeAttach function:

[ ndisFInvokeAttach function ]

Where NdisFilterHandle in the FilterAttach handler is _NDIS_FILTER_BLOCK:

_NDIS_FILTER_BLOCK  
Header                : _NDIS_OBJECT_HEADER  
NextFilter            : Ptr64 _NDIS_FILTER_BLOCK  
FilterDriver          : Ptr64 _NDIS_FILTER_DRIVER_BLOCK  
FilterModuleContext   : Ptr64 Void  
Miniport              : Ptr64 _NDIS_MINIPORT_BLOCK

Field Miniport of the _NDIS_FILTER_BLOCK: is what we need!

Pass _NDIS_FILTER_BLOCK.Miniport to the NdisMSetInterfaceCompartment along with our own created compartment guid and Miniport which belonged to its interface will be moved and appear in our compartment! In this way we can move almost any adapter-interface to an any compartment!

[ Modified Microsoft LWF example ]

To route traffic through our compartment we can use SetCurrentThreadCompartmentId for user mode code, and NdisSetThreadObjectCompartmentId for kernel mode code. We should invoke these functions at the very beginning before using any network functions (connect, send, recv).

Twitter, Facebook