Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 66 additions & 5 deletions src/Config/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,17 @@ final class Config
*/
public static function loadSystemConfigBlocking()
{
// Use WMIC output on Windows
/* Use WMIC or PowerShell on Windows
* WMIC is faster where available, but was removed in Windows 11 24H2+
* PowerShell is slower, but is available on all Windows versions
*/
if (DIRECTORY_SEPARATOR === '\\') {
return self::loadWmicBlocking();
$config = self::loadWmicBlocking();
if ($config->nameservers) {
return $config;
}

return self::loadPowershellBlocking();;
}

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

/**
* Loads the DNS configurations using Windows PowerShell
*
* Note that this method blocks while loading the given command and should
* thus be used with care! While this should be relatively fast for normal
* PowerShell commands, it remains unknown if this may block under certain
* circumstances. In particular, this method should only be executed before
* the loop starts, not while it is running.
*
* Note that this method will only try to execute the given command and try to
* parse its output, irrespective of whether this command exists. In
* particular, this method requires the DnsClient module which is only available
* on Windows 8/Server 2012 and later. Currently, this will only parse valid
* nameserver entries from the command output and will ignore all other output
* without complaining.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
* @return self
* @link https://learn.microsoft.com/en-us/powershell/module/dnsclient/get-dnsclientserveraddress
*/
public static function loadPowershellBlocking($command = null)
{
$contents = shell_exec($command === null ? 'powershell -NoLogo -NoProfile -NonInteractive -Command "Get-DnsClientServerAddress | Select-Object -ExpandProperty ServerAddresses"' : $command);

$config = new self();
if ($contents !== null) {
foreach (explode("\n", $contents) as $line) {
$ip = trim($line);
if ($ip === '' || @inet_pton($ip) === false) {
continue;
}

// skip Windows placeholder DNS addresses (fec0:0:0:ffff::1, ::2, ::3)
// these are added to interfaces without explicit DNS configuration and don't resolve anything
if (preg_match('/^fec0:0:0:ffff::/i', $ip)) {
continue;
}

$config->nameservers[] = $ip;
}
$config->nameservers = array_values(array_unique($config->nameservers));
}

return $config;
}

/**
* Loads the DNS configurations from Windows's WMIC (from the given command or default command)
*
Expand All @@ -113,19 +170,23 @@ public static function loadResolvConfBlocking($path = null)
* parse its output, irrespective of whether this command exists. In
* particular, this command is only available on Windows. Currently, this
* will only parse valid nameserver entries from the command output and will
* ignore all other output without complaining.
* ignore all other output swithout complaining.
*
* Note that WMIC has been deprecated and removed in recent Windows versions
* (Windows 11 24H2+). Consider using loadPowershellBlocking() instead.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
* @return self
* @link https://ss64.com/nt/wmic.html
* @deprecated WMIC is deprecated on Windows, use loadPowershellBlocking() instead
*/
public static function loadWmicBlocking($command = null)
{
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV 2>nul' : $command);
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents ?? '', $matches);

$config = new self();
$config->nameservers = $matches[1];
Expand Down
112 changes: 112 additions & 0 deletions tests/Config/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,103 @@ public function testParsesFileAndIgnoresCommentsAndInvalidNameserverEntries()
$this->assertEquals($expected, $config->nameservers);
}

public function testLoadsFromPowershellOnWindows()
{
if (DIRECTORY_SEPARATOR !== '\\') {
// PowerShell is Windows-only tool and not supported on other platforms
// Unix is our main platform, so we don't want to report a skipped test here (yellow)
// $this->markTestSkipped('Only on Windows');
$this->expectOutputString('');
return;
}

$config = Config::loadPowershellBlocking();

$this->assertInstanceOf(Config::class, $config);
}

public function testLoadsSingleEntryFromPowershellOutput()
{
$contents = '192.168.2.1';
$expected = ['192.168.2.1'];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}

public function testLoadsEmptyListFromPowershellOutput()
{
$contents = '';
$expected = [];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}

public function testLoadsMultipleEntriesFromPowershellOutput()
{
$contents = "192.168.2.1\n192.168.2.2\n8.8.8.8";
$expected = ['192.168.2.1', '192.168.2.2', '8.8.8.8'];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}

public function testLoadsIpv6FromPowershellOutput()
{
$contents = "::1\nfe80::1\n192.168.2.1";
$expected = ['::1', 'fe80::1', '192.168.2.1'];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}

public function testIgnoresDuplicatesFromPowershellOutput()
{
$contents = "192.168.2.1\n192.168.2.1\n8.8.8.8";
$expected = ['192.168.2.1', '8.8.8.8'];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}

public function testIgnoresInvalidEntriesFromPowershellOutput()
{
$contents = "192.168.2.1\ninvalid\nlocalhost\n8.8.8.8";
$expected = ['192.168.2.1', '8.8.8.8'];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}

public function testIgnoresEmptyLinesFromPowershellOutput()
{
$contents = "192.168.2.1\n\n\n8.8.8.8\n";
$expected = ['192.168.2.1', '8.8.8.8'];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}

public function testIgnoresWindowsPlaceholderDnsFromPowershellOutput()
{
// Windows uses fec0:0:0:ffff::1/2/3 as placeholder DNS for unconfigured interfaces
$contents = "fec0:0:0:ffff::1\nfec0:0:0:ffff::2\nfec0:0:0:ffff::3\n192.168.178.1\n8.8.8.8";
$expected = ['192.168.178.1', '8.8.8.8'];

$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals($expected, $config->nameservers);
}


public function testLoadsFromWmicOnWindows()
{
if (DIRECTORY_SEPARATOR !== '\\') {
Expand All @@ -118,6 +215,21 @@ public function testLoadsFromWmicOnWindows()
$this->assertInstanceOf(Config::class, $config);
}

public function testWmicReturnsEmptyWhenCommandFails()
{
$config = Config::loadWmicBlocking($this->echoCommand(''));

$this->assertEquals([], $config->nameservers);
}

public function testPowershellFallbackReturnsValidResults()
{
$contents = "192.168.2.1\n8.8.8.8";
$config = Config::loadPowershellBlocking($this->echoCommand($contents));

$this->assertEquals(['192.168.2.1', '8.8.8.8'], $config->nameservers);
}

public function testLoadsSingleEntryFromWmicOutput()
{
$contents = '
Expand Down