ohmu
·
2026-10-15
1#include "detection/dns/dns.h"
2#include "common/netif.h"
3#include "common/debug.h"
4#include "common/windows/registry.h"
5
6#include <ws2tcpip.h>
7#include <iphlpapi.h>
8
9// The resolvers of an interface are stored in the registry, under a key named after its GUID:
10// HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{GUID} IPv4
11// HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\{GUID} IPv6
12// A statically configured `NameServer` takes precedence over the value handed out by DHCP, which is
13// the precedence the IP stack applies as well - verified against GetAdaptersAddresses() on a machine
14// with both set, where only the static list was reported.
15//
16// This replaces GetAdaptersAddresses(), which builds the unicast, anycast, multicast and DNS server
17// lists of every adapter - about 9 ms on a machine with 52 of them - even though the module only
18// reports the resolvers of the interface that carries the default route. Reading the one key that
19// matters costs about 16 us (12 us to open it, 4 us to read the value, see
20// src/common/windows/registry.c), and the module as a whole goes from about 11 ms to about 0.3 ms.
21//
22// Known limitation: a resolver learned from a router advertisement (RDNSS) is not written to the
23// registry by any Windows version, so on a SLAAC-only network (no DHCPv6 lease, no static
24// `NameServer`) IPv6 resolvers are not reported at all. GetAdaptersAddresses() reported them.
25#define FF_DNS_REGISTRY_KEY_V4 L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\"
26#define FF_DNS_REGISTRY_KEY_V6 L"SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces\\"
27
28// Enough for either key path plus the 38 characters of a GUID
29#define FF_DNS_REGISTRY_PATH_SIZE 256
30
31static void appendAddress(FFlist* results, FFstrbuf* servers, uint32_t start, uint32_t end, FFDNSShowType showType) {
32 // A link local address may carry a zone index, which the reported address never includes
33 char address[INET6_ADDRSTRLEN];
34 uint32_t length = end - start;
35 if (length >= sizeof(address)) {
36 length = sizeof(address) - 1;
37 }
38
39 uint32_t i = 0;
40 for (; i < length && servers->chars[start + i] != '%'; ++i) {
41 address[i] = servers->chars[start + i];
42 }
43 address[i] = '\0';
44
45 if (showType & FF_DNS_TYPE_IPV4_BIT) {
46 IN_ADDR ipv4;
47 if (InetPtonA(AF_INET, address, &ipv4) == 1) {
48 FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
49 ffStrbufInitA(item, INET_ADDRSTRLEN);
50 item->length = (uint32_t) (RtlIpv4AddressToStringA(&ipv4, item->chars) - item->chars);
51 return;
52 }
53 }
54
55 if (showType & FF_DNS_TYPE_IPV6_BIT) {
56 IN6_ADDR ipv6;
57 if (InetPtonA(AF_INET6, address, &ipv6) == 1) {
58 FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
59 ffStrbufInitA(item, INET6_ADDRSTRLEN);
60 item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6, item->chars) - item->chars);
61 }
62 }
63}
64
65static void appendAddressList(FFstrbuf* servers, FFDNSShowType showType, FFlist* results) {
66 uint32_t start = 0;
67 while (start < servers->length) {
68 while (start < servers->length && (servers->chars[start] == ',' || servers->chars[start] == ' ' || servers->chars[start] == '\t')) {
69 ++start;
70 }
71
72 uint32_t end = start;
73 while (end < servers->length && servers->chars[end] != ',' && servers->chars[end] != ' ' && servers->chars[end] != '\t') {
74 ++end;
75 }
76
77 if (end > start) {
78 appendAddress(results, servers, start, end, showType);
79 }
80 start = end;
81 }
82}
83
84// A DHCPv6 lease stores its resolvers as a plain list of 16 byte addresses
85static void appendAddressArray(const BYTE* data, uint32_t dataLength, FFDNSShowType showType, FFlist* results) {
86 if (!(showType & FF_DNS_TYPE_IPV6_BIT)) {
87 return;
88 }
89
90 for (uint32_t offset = 0; offset + sizeof(IN6_ADDR) <= dataLength; offset += sizeof(IN6_ADDR)) {
91 IN6_ADDR ipv6;
92 memcpy(&ipv6, data + offset, sizeof(ipv6));
93
94 FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
95 ffStrbufInitA(item, INET6_ADDRSTRLEN);
96 item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6, item->chars) - item->chars);
97 }
98}
99
100// `Dhcpv6InterfaceOptions` stores the DHCPv6 options of the last lease as an array of records: a 20
101// byte header (option code, 0, payload length, 0, 0xFFFFFFFF) followed by the payload, padded to 16
102// bytes. Option 23 is OPTION_DNS_SERVERS (RFC 3646).
103static void appendDhcpv6Options(const BYTE* data, uint32_t dataLength, FFDNSShowType showType, FFlist* results) {
104 for (uint32_t offset = 0; offset + 20 <= dataLength;) {
105 uint32_t code, length;
106 memcpy(&code, data + offset, sizeof(code));
107 memcpy(&length, data + offset + 8, sizeof(length));
108
109 uint32_t payloadLength = dataLength - (offset + 20);
110 if (payloadLength > length) {
111 payloadLength = length;
112 }
113
114 if (code == 23) {
115 appendAddressArray(data + offset + 20, payloadLength, showType, results);
116 return;
117 }
118
119 if (payloadLength < length) {
120 return;
121 }
122 offset += 20 + ((payloadLength + 15) & ~(uint32_t) 15);
123 }
124}
125
126static void detectV4(const wchar_t* guid, FFDNSOptions* options, FFlist* results) {
127 wchar_t keyPath[FF_DNS_REGISTRY_PATH_SIZE] = FF_DNS_REGISTRY_KEY_V4;
128 wcscat(keyPath, guid);
129 FF_DEBUG("v4 key '%ls'", keyPath);
130
131 FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
132 if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, keyPath, &hKey, nullptr)) {
133 FF_DEBUG("v4 key open failed");
134 return;
135 }
136
137 FF_STRBUF_AUTO_DESTROY servers = ffStrbufCreate();
138 // `NameServer` holds the statically configured list and wins over the one handed out by DHCP. A
139 // value that exists but holds no address must not shadow the fallback, hence the length checks.
140 bool got = (ffRegReadStrbuf(hKey, L"NameServer", &servers, nullptr) && servers.length) ||
141 (ffRegReadStrbuf(hKey, L"DhcpNameServer", &servers, nullptr) && servers.length);
142 FF_DEBUG("v4 got=%d servers='%s'", got, servers.chars);
143 if (!got) {
144 return;
145 }
146
147 appendAddressList(&servers, options->showType, results);
148 FF_DEBUG("v4 results=%u", results->length);
149}
150
151static void detectV6(const wchar_t* guid, FFDNSOptions* options, FFlist* results) {
152 wchar_t keyPath[FF_DNS_REGISTRY_PATH_SIZE] = FF_DNS_REGISTRY_KEY_V6;
153 wcscat(keyPath, guid);
154 FF_DEBUG("v6 key '%ls'", keyPath);
155
156 FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
157 if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, keyPath, &hKey, nullptr)) {
158 FF_DEBUG("v6 key open failed");
159 return;
160 }
161
162 FF_STRBUF_AUTO_DESTROY servers = ffStrbufCreate();
163 if (ffRegReadStrbuf(hKey, L"NameServer", &servers, nullptr) && servers.length) {
164 FF_DEBUG("v6 servers='%s'", servers.chars);
165 appendAddressList(&servers, options->showType, results);
166 FF_DEBUG("v6 results=%u", results->length);
167 return;
168 }
169
170 // The two DHCPv6 values below are documented as REG_BINARY and are always written that way; the
171 // registry reader reports the bytes without their type, so the parsing below trusts that.
172 BYTE data[4096];
173
174 FFArgBuffer dhcpv6Servers = { .data = data, .length = sizeof(data) };
175 if (ffRegReadData(hKey, L"Dhcpv6DNSServers", &dhcpv6Servers, nullptr)) {
176 FF_DEBUG("v6 Dhcpv6DNSServers cb=%u", dhcpv6Servers.length);
177 appendAddressArray(data, dhcpv6Servers.length, options->showType, results);
178 return;
179 }
180
181 FFArgBuffer dhcpv6Options = { .data = data, .length = sizeof(data) };
182 if (ffRegReadData(hKey, L"Dhcpv6InterfaceOptions", &dhcpv6Options, nullptr)) {
183 FF_DEBUG("v6 Dhcpv6InterfaceOptions cb=%u", dhcpv6Options.length);
184 appendDhcpv6Options(data, dhcpv6Options.length, options->showType, results);
185 return;
186 }
187
188 FF_DEBUG("v6 no resolver");
189}
190
191const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) {
192 FF_DEBUG("enter, showType=%u", (unsigned) options->showType);
193 // The interface that carries the default route is the one the IP stack resolves names through.
194 // ffNetifGetDefaultRouteV4() already ignores the interfaces that are not operational.
195 NET_IFINDEX ifIndex = (NET_IFINDEX) ffNetifGetDefaultRouteV4()->ifIndex;
196 FF_DEBUG("ifIndex=%u", (unsigned) ifIndex);
197 if (!ifIndex) {
198 ifIndex = (NET_IFINDEX) ffNetifGetDefaultRouteV6()->ifIndex;
199 }
200 if (!ifIndex) {
201 FF_DEBUG("Failed to detect default route");
202 return "Failed to detect default route";
203 }
204
205 NET_LUID luid;
206 DWORD ret = ConvertInterfaceIndexToLuid(ifIndex, &luid);
207 if (ret != NO_ERROR) {
208 FF_DEBUG("ConvertInterfaceIndexToLuid() failed: %s", ffDebugWin32Error(ret));
209 return "ConvertInterfaceIndexToLuid() failed";
210 }
211
212 GUID guid;
213 ret = ConvertInterfaceLuidToGuid(&luid, &guid);
214 if (ret != NO_ERROR) {
215 FF_DEBUG("ConvertInterfaceLuidToGuid() failed: %s", ffDebugWin32Error(ret));
216 return "ConvertInterfaceLuidToGuid() failed";
217 }
218
219 wchar_t guidStr[64];
220 // clang-format off
221 swprintf(guidStr, ARRAY_SIZE(guidStr), L"{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}",
222 (unsigned long) guid.Data1,
223 (unsigned int) guid.Data2,
224 (unsigned int) guid.Data3,
225 (unsigned int) guid.Data4[0],
226 (unsigned int) guid.Data4[1],
227 (unsigned int) guid.Data4[2],
228 (unsigned int) guid.Data4[3],
229 (unsigned int) guid.Data4[4],
230 (unsigned int) guid.Data4[5],
231 (unsigned int) guid.Data4[6],
232 (unsigned int) guid.Data4[7]);
233 // clang-format on
234
235 // IPv4 first, so the list is grouped by address family the same way the printed output is
236 if (options->showType & FF_DNS_TYPE_IPV4_BIT) {
237 detectV4(guidStr, options, results);
238 }
239
240 if (options->showType & FF_DNS_TYPE_IPV6_BIT) {
241 detectV6(guidStr, options, results);
242 }
243
244 FF_DEBUG("return, results=%u", results->length);
245 return nullptr;
246}