master ohmu/fastfetch-openbsd / src / detection / dns / dns_windows.c
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}