The number of patches here looks big, but 4 of 8 patches are trivial so I am hoping that maybe it is ok.
Regarding patch 4 ("nsi: Cache nsi device handle."), looks like Windows has one cached handle for everything. First of all, NotifyAddrChange docs suggest something like that [1] in the warning regarding the returned handle: "Warning Do not close this handle, and do not associate it with a completion port.". And it looks like that would be really bad idea. I tested that by putting my added test_change_notifications() first between tests and closing the handle in the end of it, the consequent tests which try to call Nsi functions are broken then. Yet in patch 5 ("nsi: Forward request to nsiproxy from NsiRequestChangeNotification().") I am creating a different device handle for async use in NsiRequestChangeNotification(). Otherwise DeviceIoControl calls in the other functions are not fully valid without providing overlapped structure with an event and handling overlapped result (even if that doesn't currently lead to problems in practice because nsiproxy.sys handles those requests sync hronously and those DeviceIoControl's complete synchronously anyway). It seems to me it is better to use different device handles rather than create and close event for each request and wait for async result.
So far I don't have anything for the other iphlpapi notification functions (e. g., NotifyIpInterfaceChange). I only tested that those notifications are always delivered in the same thread which thread is not the same as from where NotifyIpInterfaceChange was called. So probably one possible way to implement that is to have the notification thread in nsi.dll which will use NsiRequestChangeNotification on the appropriate tables, poll for async result on a completion port and call notifications functions. That will, however, require some additional data returned from nsiproxy.dll (possibly from the same ioctl) to indicate the changed row needed as a callback parameter.
Not related to the patches themselves, NsiRequestChangeNotification / NsiCancelChangeNotification (as well as all the other Nsi functions) are completely undocumented, so maybe it is interesting to share how I deduced the parameters. First of all, I measured stack pointer difference resulting from these WINAPI function calls on i386, and that is 20 bytes (5 parameters) for NsiRequestChangeNotification and 4 bytes (one parameter) for NsiCancelChangeNotification. It is pretty obvious that for the latter it could only be overlapped pointer, no other way to cancel the notification. For NsiRequestChangeNotification, calling with all NULL results in access violation on the user side. I found that it is only 4th parameter being NULL causes such an access violation. And that was access violation for write, requiring 20 bytes writeable, and, most notably, upon returning the error with this parameter set the first dword was set to 0x103, which directly suggested that 4th parameter is OVERLAPPE D *. I was pretty sure that output handle should most likely go as either first or last parameter, so there were 3 left. Then, shuffling the parameters, I could get errors ERROR_NOACCESS (suggesting some bad pointer accessed on the kernel side) and ERROR_NOT_SUPPORTED (which was interesting in its old way). I figured that ERROR_NOACCESS is due to the 2nd parameter requiring 24 bytes of readable memory. At this point I payed more attention to the existing Nsi function prototypes (and also ERROR_NOT_SUPPORTED was suggesting that my request is maybe gets routed to the wrong module) and it became apparent what could be there (the thing works with the first parameter being 0, with 1 it returns ERROR_INVALID_PARAMETER and I didn't test that exhaustively).
1. https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-not...