Home About Me

The Windows QoS Socket Traps That Turned a Five-Minute Fix into Three and a Half Weeks

Adding QoS to an existing client/server socket application looked straightforward. The sample code was short, but getting it to work in practice took three and a half weeks. The final problem alone remained unresolved for nearly half a month.

These are the problems that ultimately mattered, along with the fixes that worked.

1. QOSCreateHandle fails with ERROR_NOT_SUPPORTED (50)

The first symptom was a failure from the QoS handle creation wrapper, with GetLastError() returning ERROR_NOT_SUPPORTED (50).

The cause was an outdated example. Older code passed {1, 1} as the first argument to QOSCreateHandle. Since Windows 10, the version value expected there is {1, 0}.

Use a QOS_VERSION value of {1, 0} instead:

m_ver({1, 0})

2. QOSAddSocketToFlow fails with WSA_INVALID_PARAMETER (87)

There were two separate issues behind this error.

First, some older examples passed 0 as the fifth argument. Newer versions of the function accept only the defined flags QOS_NON_ADAPTIVE_FLOW and QOS_QUERYFLOW_FRESH; 0 is not valid. For this use case, the correct value is QOS_NON_ADAPTIVE_FLOW.

Second, the PC used for testing had two network adapters, and the internal network was connected through the second adapter. Passing NULL as the target address therefore did not work reliably. The address had to be supplied explicitly through a SOCKADDR structure containing the IP address and port, so that the intended adapter could be selected.

After the handshake succeeds, the peer IP address and port can be obtained through CAsyncSocket::GetPeerName(). The fifth argument should be fixed to QOS_NON_ADAPTIVE_FLOW.

3. Running the server and client on the same machine produces ERROR_NOT_FOUND (1168)

When the server and client were started together on one PC, the client could fail in QOSAddSocketToFlow with ERROR_NOT_FOUND (1168).

The reason is that a socket being added to a QoS flow cannot use a client connection to a server running on the same machine in this setup. The practical solution was to test with another development machine.

The server also could not use 127.0.0.1. It is unclear whether that restriction was related to the presence of multiple network adapters, but using the actual network address was necessary.

4. Adding QoS after the handshake returns ERROR_ACCESS_DENIED (5)

The QoS call was initially placed in the callback that confirmed the socket handshake. On the client, QOSAddSocketToFlow then failed with ERROR_ACCESS_DENIED (5).

A server using this QoS configuration must run with administrator privileges. During debugging, Visual Studio needs to be launched as administrator on the server side. Another option is to change the project settings under:

Linker → Manifest File → UAC level

Set it to:

requireAdministrator (/level='requireAdministrator')

This requirement was easy to miss because it did not appear in every example. It was eventually found in the description of a sample project.

5. OnConnect sometimes reports WSAEWOULDBLOCK (10035)

The client's OnConnect(int nErrorCode) callback sometimes received WSAEWOULDBLOCK (10035).

This is not necessarily a failure. It only indicates that the socket handshake was delayed. There is no special recovery step required; waiting for the connection process to complete is enough.

In other words, this error code is not the same kind of problem as the connection-aborted error described below, even though both can appear during connection setup and are easy to confuse.

6. The server immediately receives OnClose() with WSAECONNABORTED (10053)

This was the most time-consuming problem.

After the socket connection was established, the server immediately received OnClose(), with the callback parameter set to WSAECONNABORTED (10053). The existing client code called SetSockOpt() immediately after creating the socket and configured SO_LINGER as {1, 0}. That setting tells the socket not to wait for buffered data during close, but to perform an immediate hard close.

The problem was not simply the SO_LINGER value. The option had been set before the socket handshake completed. When QOSAddSocketToFlow() was called, the combination of the pending handshake, QoS configuration, and early SO_LINGER setting caused the connection to close in this way.

The fix was to move the SetSockOpt() call until after the connection had been established:

  • On the client, call it after OnConnect(0).
  • On the server, call it after OnAccept(0).

The value {1, 0} can remain unchanged; the important part is when the option is applied.

The debugging process for this issue was especially frustrating. The interaction between the socket state, QoS setup, administrator permissions, network adapters, and connection behavior made antivirus software, the firewall, and domain policies seem like possible causes. Eventually, commenting out the existing code line by line isolated SO_LINGER. There did not appear to be another matching case in the available examples, possibly because few applications set SO_LINGER before the connection had completed.

A shared QoS socket class

The common socket class inherited from CAsyncSocket. It creates the QoS handle using version {1, 0}, obtains the peer address when no explicit address is supplied, adds the socket to a non-adaptive QoS flow, and removes that flow before closing the socket.

#pragma once #include <afxsock.h> #include <qossp.h> #include <winsock2.h> #include <qos2.h> #include <iostream> #pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "qwave.lib") class CCommonQosSocket : public CAsyncSocket { public: CCommonQosSocket() : m_hQos(NULL) , m_dwFlowId(0) , m_ver({1, 0}){} virtual ~CCommonQosSocket() { CloseWithQos(); } BOOL CreateQosHandle() { if (m_hQos) { QOSCloseHandle(m_hQos); m_hQos = NULL; } if (!QOSCreateHandle(&m_ver, &m_hQos)) { int nLastError = GetLastError(); return FALSE; } return TRUE; } BOOL GetPeerAddr(SOCKADDR_IN& peerAddr) { int len = sizeof(peerAddr); if (!GetPeerName((SOCKADDR*)&peerAddr, &len)) { int n = GetLastError(); return FALSE; } return TRUE; } BOOL AddQosFlow(QOS_TRAFFIC_TYPE trafficType, SOCKADDR* pAddr) { if (!m_hQos || !m_dwFlowId) { return FALSE; } SOCKADDR* pTgtAddr(pAddr); SOCKADDR_IN peerAddr{}; if (!pTgtAddr) { if (!GetPeerAddr(peerAddr)) { return FALSE; } pTgtAddr = static_cast<SOCKADDR*>(&peerAddr); } BOOL bRet = QOSAddSocketToFlow(m_hQos, static_cast<SOCKET>(*this), pTgtAddr, trafficType, QOS_NON_ADAPTIVE_FLOW, &m_dwFlowId); int nLastError = GetLastError(); return bRet; } void CloseWithQos() { if (m_dwFlowId && m_hQos) { QOSRemoveSocketFromFlow(m_hQos, static_cast<SOCKET>(*this), m_dwFlowId, 0); } if (m_hQos) { QOSCloseHandle(m_hQos); m_hQos = NULL; } m_dwFlowId = 0; __super::Close(); } private: HANDLE m_hQos; DWORD m_dwFlowId; QOS_VERSION m_ver; };

Server-side sample

On the server, the QoS flow is added after Accept() succeeds. The peer address returned by Accept() is passed into AddQosFlow(). The linger option is applied only after the QoS flow has been added successfully.

#include "CommonQosSocket.h" class CClientSocket: public CCommonQoSSocket { }; class CListenSockt : public CCommonQoSSocket { public: virtual void OnAccept(int nErrorCode) override { CAsyncSocket::OnAccept(nErrorCode); CClientSocket* pNewClient = new CClientSocket; sockaddr addr; int iAddrLen = sizeof(addr); if (Accept(*pNewClient, &addr, &iAddrLen)) { pNewClient->CreateQosHandle(); if (pNewClient->AddQosFlow(QOSTrafficTypeBestEffort, &addr)) //sccess; linger closeLinger{1,0}; (void)pNewClient->SetSockOpt(SO_LINGER, (const void*)&closeLinger, sizeof linger); else { //failed } } }; }; void CQoSServerDlg::OnBnClickedButtonStart() { CClientSocket* pListen = new CClientSocket; CString csLocalIP(L"192.168.8.4"); int nListenPort(32000); if (!pListen->Create(nListenPort, SOCK_STREAM, FD_READ | FD_WRITE | FD_ACCEPT | FD_CLOSE, csLocalIP)) { int nError = GetLastError(); AfxMessageBox(L"Listen Failed."); return; } if (!m_ListenSock.Listen()) { AfxMessageBox(L"Listen Failed."); return; } }

Client-side sample

The client creates its QoS handle before connecting, then adds the socket to the flow from OnConnect(0). Because the peer address is not passed explicitly, the shared class obtains it through GetPeerName().

#include "CommonQosSocket.h" class CClientSocket: public CCommonQoSSocket { public: virtual void OnConnect(int nErrorCode) overwride { CAsyncSocket::OnConnect(nErrorCode); if (nErrorCode) { return; } CreateQosHandle(); if (this->AddQosFlow(QOSTrafficTypeBestEffort, nullptr)) { //success linger closeLinger{1,0}; (void)this->SetSockOpt(SO_LINGER, (const void*)&closeLinger, sizeof linger); } else { //failed } } }; void CQoSClientDlg::OnBnClickedButtonConnect() { CClientSocket* pClient = new CClientSocket; pClient->CreateQosHandle(); CString csLocal(L"192.168.8.11"); CString csServer(L"192.168.8.4"); int nPort(32000); pClient->Create(0, SOCK_STREAM, FD_READ | FD_WRITE | FD_CONNECT | FD_CLOSE, csLocal); if (m_sock.Connect(csServer, nPort)) { } else { } }

The first two errors were found by comparing newer and older examples. The administrator requirement emerged from a sample's usage notes, while the SO_LINGER issue required systematically removing code until the cause became visible. In the end, diagnosing the last problem took thirteen days; changing the actual code took about five minutes.