Skip to content

Commit fc37a05

Browse files
committed
Use Get-DnsClientServerAddress via Powershell on Windows as an alternative to deprecated wmic
1 parent 772f1e9 commit fc37a05

File tree

2 files changed

+178
-5
lines changed

2 files changed

+178
-5
lines changed

src/Config/Config.php

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,17 @@ final class Config
2828
*/
2929
public static function loadSystemConfigBlocking()
3030
{
31-
// Use WMIC output on Windows
31+
/* Use WMIC or PowerShell on Windows
32+
* WMIC is faster where available, but was removed in Windows 11 24H2+
33+
* PowerShell is slower, but is available on all Windows versions
34+
*/
3235
if (DIRECTORY_SEPARATOR === '\\') {
33-
return self::loadWmicBlocking();
36+
$config = self::loadWmicBlocking();
37+
if ($config->nameservers) {
38+
return $config;
39+
}
40+
41+
return self::loadPowershellBlocking();;
3442
}
3543

3644
// otherwise (try to) load from resolv.conf
@@ -100,6 +108,55 @@ public static function loadResolvConfBlocking($path = null)
100108
return $config;
101109
}
102110

111+
/**
112+
* Loads the DNS configurations using Windows PowerShell
113+
*
114+
* Note that this method blocks while loading the given command and should
115+
* thus be used with care! While this should be relatively fast for normal
116+
* PowerShell commands, it remains unknown if this may block under certain
117+
* circumstances. In particular, this method should only be executed before
118+
* the loop starts, not while it is running.
119+
*
120+
* Note that this method will only try to execute the given command and try to
121+
* parse its output, irrespective of whether this command exists. In
122+
* particular, this method requires the DnsClient module which is only available
123+
* on Windows 8/Server 2012 and later. Currently, this will only parse valid
124+
* nameserver entries from the command output and will ignore all other output
125+
* without complaining.
126+
*
127+
* Note that the previous section implies that this may return an empty
128+
* `Config` object if no valid nameserver entries can be found.
129+
*
130+
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
131+
* @return self
132+
* @link https://learn.microsoft.com/en-us/powershell/module/dnsclient/get-dnsclientserveraddress
133+
*/
134+
public static function loadPowershellBlocking($command = null)
135+
{
136+
$contents = shell_exec($command === null ? 'powershell -NoLogo -NoProfile -NonInteractive -Command "Get-DnsClientServerAddress | Select-Object -ExpandProperty ServerAddresses"' : $command);
137+
138+
$config = new self();
139+
if ($contents !== null) {
140+
foreach (explode("\n", $contents) as $line) {
141+
$ip = trim($line);
142+
if ($ip === '' || @inet_pton($ip) === false) {
143+
continue;
144+
}
145+
146+
// skip Windows placeholder DNS addresses (fec0:0:0:ffff::1, ::2, ::3)
147+
// these are added to interfaces without explicit DNS configuration and don't resolve anything
148+
if (preg_match('/^fec0:0:0:ffff::/i', $ip)) {
149+
continue;
150+
}
151+
152+
$config->nameservers[] = $ip;
153+
}
154+
$config->nameservers = array_values(array_unique($config->nameservers));
155+
}
156+
157+
return $config;
158+
}
159+
103160
/**
104161
* Loads the DNS configurations from Windows's WMIC (from the given command or default command)
105162
*
@@ -113,19 +170,23 @@ public static function loadResolvConfBlocking($path = null)
113170
* parse its output, irrespective of whether this command exists. In
114171
* particular, this command is only available on Windows. Currently, this
115172
* will only parse valid nameserver entries from the command output and will
116-
* ignore all other output without complaining.
173+
* ignore all other output swithout complaining.
174+
*
175+
* Note that WMIC has been deprecated and removed in recent Windows versions
176+
* (Windows 11 24H2+). Consider using loadPowershellBlocking() instead.
117177
*
118178
* Note that the previous section implies that this may return an empty
119179
* `Config` object if no valid nameserver entries can be found.
120180
*
121181
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
122182
* @return self
123183
* @link https://ss64.com/nt/wmic.html
184+
* @deprecated WMIC is deprecated on Windows, use loadPowershellBlocking() instead
124185
*/
125186
public static function loadWmicBlocking($command = null)
126187
{
127-
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
128-
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
188+
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV 2>nul' : $command);
189+
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents ?? '', $matches);
129190

130191
$config = new self();
131192
$config->nameservers = $matches[1];

tests/Config/ConfigTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,103 @@ public function testParsesFileAndIgnoresCommentsAndInvalidNameserverEntries()
103103
$this->assertEquals($expected, $config->nameservers);
104104
}
105105

106+
public function testLoadsFromPowershellOnWindows()
107+
{
108+
if (DIRECTORY_SEPARATOR !== '\\') {
109+
// PowerShell is Windows-only tool and not supported on other platforms
110+
// Unix is our main platform, so we don't want to report a skipped test here (yellow)
111+
// $this->markTestSkipped('Only on Windows');
112+
$this->expectOutputString('');
113+
return;
114+
}
115+
116+
$config = Config::loadPowershellBlocking();
117+
118+
$this->assertInstanceOf(Config::class, $config);
119+
}
120+
121+
public function testLoadsSingleEntryFromPowershellOutput()
122+
{
123+
$contents = '192.168.2.1';
124+
$expected = ['192.168.2.1'];
125+
126+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
127+
128+
$this->assertEquals($expected, $config->nameservers);
129+
}
130+
131+
public function testLoadsEmptyListFromPowershellOutput()
132+
{
133+
$contents = '';
134+
$expected = [];
135+
136+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
137+
138+
$this->assertEquals($expected, $config->nameservers);
139+
}
140+
141+
public function testLoadsMultipleEntriesFromPowershellOutput()
142+
{
143+
$contents = "192.168.2.1\n192.168.2.2\n8.8.8.8";
144+
$expected = ['192.168.2.1', '192.168.2.2', '8.8.8.8'];
145+
146+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
147+
148+
$this->assertEquals($expected, $config->nameservers);
149+
}
150+
151+
public function testLoadsIpv6FromPowershellOutput()
152+
{
153+
$contents = "::1\nfe80::1\n192.168.2.1";
154+
$expected = ['::1', 'fe80::1', '192.168.2.1'];
155+
156+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
157+
158+
$this->assertEquals($expected, $config->nameservers);
159+
}
160+
161+
public function testIgnoresDuplicatesFromPowershellOutput()
162+
{
163+
$contents = "192.168.2.1\n192.168.2.1\n8.8.8.8";
164+
$expected = ['192.168.2.1', '8.8.8.8'];
165+
166+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
167+
168+
$this->assertEquals($expected, $config->nameservers);
169+
}
170+
171+
public function testIgnoresInvalidEntriesFromPowershellOutput()
172+
{
173+
$contents = "192.168.2.1\ninvalid\nlocalhost\n8.8.8.8";
174+
$expected = ['192.168.2.1', '8.8.8.8'];
175+
176+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
177+
178+
$this->assertEquals($expected, $config->nameservers);
179+
}
180+
181+
public function testIgnoresEmptyLinesFromPowershellOutput()
182+
{
183+
$contents = "192.168.2.1\n\n\n8.8.8.8\n";
184+
$expected = ['192.168.2.1', '8.8.8.8'];
185+
186+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
187+
188+
$this->assertEquals($expected, $config->nameservers);
189+
}
190+
191+
public function testIgnoresWindowsPlaceholderDnsFromPowershellOutput()
192+
{
193+
// Windows uses fec0:0:0:ffff::1/2/3 as placeholder DNS for unconfigured interfaces
194+
$contents = "fec0:0:0:ffff::1\nfec0:0:0:ffff::2\nfec0:0:0:ffff::3\n192.168.178.1\n8.8.8.8";
195+
$expected = ['192.168.178.1', '8.8.8.8'];
196+
197+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
198+
199+
$this->assertEquals($expected, $config->nameservers);
200+
}
201+
202+
106203
public function testLoadsFromWmicOnWindows()
107204
{
108205
if (DIRECTORY_SEPARATOR !== '\\') {
@@ -118,6 +215,21 @@ public function testLoadsFromWmicOnWindows()
118215
$this->assertInstanceOf(Config::class, $config);
119216
}
120217

218+
public function testWmicReturnsEmptyWhenCommandFails()
219+
{
220+
$config = Config::loadWmicBlocking($this->echoCommand(''));
221+
222+
$this->assertEquals([], $config->nameservers);
223+
}
224+
225+
public function testPowershellFallbackReturnsValidResults()
226+
{
227+
$contents = "192.168.2.1\n8.8.8.8";
228+
$config = Config::loadPowershellBlocking($this->echoCommand($contents));
229+
230+
$this->assertEquals(['192.168.2.1', '8.8.8.8'], $config->nameservers);
231+
}
232+
121233
public function testLoadsSingleEntryFromWmicOutput()
122234
{
123235
$contents = '

0 commit comments

Comments
 (0)