Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active January 28, 2024 22:54
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jaykul/315f7413752a92997390bf2ef052bc17 to your computer and use it in GitHub Desktop.
Save Jaykul/315f7413752a92997390bf2ef052bc17 to your computer and use it in GitHub Desktop.
Some functions I wrote to fix WSL problems

WSL Helper Functions

This is a collection of helpers I'm writing to solve WSL problems:

  1. Update-WslDns because there are a lot of problems with DNS out there (it's on the WSL FAQ).
  2. Update-WslCertificates because sometimes new CA certs (like for ZScaler) don't show up "trusted" in WSL.
  3. Invoke-Wsl because wsl --list output is incorrectly encoded and can't be used as-is.
  4. Disable-WslGenerateResolveConf so you can make the Update-WslDns changes permanent.

There are a couple of other helper functions in here. Here's some autogenerated help:

Disable-WslGenerateResolvConf

Update wsl.conf to disable generateResolvConf.

Invoke-Wsl

Wrap wsl.exe with Console.Encoding because it ignores Console.Encoding. (This way, it's output matches Console.Encoding and is handled by PowerShell).

Set-WslContent

A wrapper for piping content into WSL files that aren't writeable as the default user.

Update-WslCertificates

Copy certificates from LocalMachine\Root to a WSL distro. Thanks ZScaler Internet Security. 😑

Update-WslResolv

Update the DNS servers on WSL distros to fix connectivity when VPNs mess with it. docs.microsoft.com/.../troubleshooting#bash-loses-network-connectivity-once-connected-to-a-vpn

Bonus Functions

There are a couple of functions for parsing ini files which I used with the wsl.conf files

ConvertFrom-IniContent

Parses content from ini/conf files into nested hashtables.

@"
[automount]
enabled = true
mountFsTab = true

[network]
generateResolvConf = false
"@ | ConvertFrom-IniContent    
    
Name                           Value
----                           -----
automount                      {enabled, mountFsTab}
network                        {generateResolvConf}

ConvertTo-IniContent

Convert nested hashtables to ini syntax. Supports recursively nested hashtables by putting dots in the section names.

@{
  automount = @{
    enabled = $true
    mountFsTab = $true
  }
  network = @{
    generateResolvConf = $false
  }
} | ConvertTo-IniContent
    
[automount]
enabled = True
mountFsTab = True
[network]
generateResolvConf = False
function ConvertFrom-IniContent {
<#
.SYNOPSIS
Parses content from ini/conf files into nested hashtables.
.EXAMPLE
Get-Content \\wsl$\Ubuntu\etc\wsl.conf | ConvertFrom-IniContent
.EXAMPLE
ConvertFrom-IniContent (Get-Content ~\.wslconf -Raw)
.EXAMPLE
"
[automount]
enabled = true
mountFsTab = true
[network]
generateResolvConf = false
" | ConvertFrom-IniContent
Name Value
---- -----
automount {enabled, mountFsTab}
network {generateResolvConf}
#>
[CmdletBinding()]
param(
# The content of an ini file
[Parameter(Mandatory, ValueFromPipeline)]
[AllowEmptyString()]
[string]$InputObject
)
begin {
$StringBuilder = [System.Collections.Generic.List[string]]::new()
}
process {
$StringBuilder.AddRange([string[]]@($InputObject -split "[\r\n]+"))
}
end {
$ini = [ordered]@{}
switch -regex ($StringBuilder) {
"^\s*\[(.*)\]\s*$" {
$section = $ini
foreach ($level in $matches[1] -split "\.") {
if (!$section[$level]) {
$section[$level] = [ordered]@{}
}
$section = $section[$level]
}
}
"^\s*(.+?)\s*=\s*(.+?)\s*$" {
$name, $value = $matches[1..2]
$section[$name] = $value
}
}
$ini
}
}
function ConvertTo-IniContent {
<#
.SYNOPSIS
Convert nested hashtables to ini syntax.
Supports recursively nested hashtables by putting dots in the section names.
.EXAMPLE
Get-Content \\wsl$\Ubuntu\etc\wsl.conf | ConvertFrom-IniContent
.EXAMPLE
ConvertFrom-IniContent (Get-Content ~\.wslconf -Raw)
.EXAMPLE
@{
automount = @{
enabled = $true
mountFsTab = $true
}
network = @{
generateResolvConf = $false
}
} | ConvertTo-IniContent
[automount]
enabled = True
mountFsTab = True
[network]
generateResolvConf = False
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory, ValueFromPipeline, Position = 0)]
[System.Collections.IDictionary]$InputObject,
[Parameter()]
[string[]]$Section
)
process {
if ($Section) { "[$($Section -join ".")]" }
$after = @()
foreach ($kv in $InputObject.GetEnumerator()) {
if ($kv.value -is [System.Collections.IDictionary]) {
$after += $kv
} else {
$kv.key + " = " + $kv.value
}
}
foreach ($kv in $after) {
$Nested = $section + $kv.Key
$kv.value | ConvertTo-IniContent -Section $Nested
}
}
}
function Set-WslContent {
# .SYNOPSIS
# A wrapper for piping content into WSL files that aren't writeable as the default user.
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
[CmdletBinding()]
param(
# The name of the linux distro
[Parameter()]
[string]$Distribution,
# The linux file path
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[ValidatePattern('^[^"]*$')]
[Alias("Content")]
[string[]]$InputObject
)
$ENV:WSLPASSCONTENT = $InputObject -join "`n"
$env:WSLENV += ":WSLPASSCONTENT"
if ($Distribution) {
wsl --distribution $Distribution -u root sh -c "cat << EOF > $Path`n`${WSLPASSCONTENT}`nEOF"
} else {
wsl -u root sh -c "cat << EOF > $Path`n`${WSLPASSCONTENT}`nEOF"
}
$env:WSLENV = $env:WSLENV -replace ":WSLPASSCONTENT$"
}
function Update-WslCertificates {
<#
.SYNOPSIS
Copy certificates from LocalMachine\Root to a WSL distro.
Thanks ZScaler Internet Security. 😑
#>
[CmdletBinding()]
param(
# Certificate thumbprints to copy into WSL distros for trust (for example, this one from ZScaler)
[Parameter(ValueFromPipeline)]
[array]$Certificates = "d72f47d87420e3f0f9bdcac6f03a566743c481b9",
# The Distribution to configure (by default, all of them)
# Be careful when calculating the values for this:
# There is current a bug in wsl that causes it to output nulls after every character
[string[]]$Distribution = $(wsl --list --quiet)
)
begin {
$AllCertificates = [System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]]::new()
}
process {
[System.Security.Cryptography.X509Certificates.X509Certificate2[]]$MoreCertificates =
@($Certificates).Where{ $_ -is [System.Security.Cryptography.X509Certificates.X509Certificate2] }
$null = $AllCertificates.AddRange($MoreCertificates)
if (($CertificateNames = @($Certificates).Where{ $_ -is [string] })) {
Push-Location Cert:\LocalMachine\Root
$MoreCertificates = @(Get-Item $CertificateNames -ErrorAction Ignore).Where{ $_ -is [System.Security.Cryptography.X509Certificates.X509Certificate2] }
$null = $AllCertificates.AddRange($MoreCertificates)
Pop-Location
}
}
end {
foreach ($distro in $Distribution) {
if (!(wsl --distribution $distro which update-ca-certificates)) {
Write-Warning "$distro missing update-ca-certificates -- not updating Trusted.pem"
continue
}
$AllCertificates | ForEach-Object {
if (!($Name = $_.Subject -replace ".*CN=([^,]*).*", '$1' -replace " ", "_")) {
$Name = $_.Thumbprint
}
Write-Information "Adding Certificate $($_.Thumbprint) for $($Name)" -InformationAction Continue
$rawcert = @(
"-----BEGIN CERTIFICATE-----"
[Convert]::ToBase64String($_.RawData)
"-----END CERTIFICATE-----"
)
Set-WslContent -Distribution $distro -Path "/usr/local/share/ca-certificates/$Name.crt" -Content $rawcert
}
wsl --distribution $distro -u root update-ca-certificates
}
}
}
function Update-WslDns {
<#
.SYNOPSIS
Update the DNS servers on WSL distros to fix connectivity when VPNs mess with it.
https://docs.microsoft.com/en-us/windows/wsl/troubleshooting#bash-loses-network-connectivity-once-connected-to-a-vpn
.DESCRIPTION
Since this reaches into each distro and runs commands with `sudo` one at a time,
You MIGHT want to use `sudo visudo` on each distro to remove the password request from sudo to make this go smoothly.
%sudo ALL=(ALL:ALL) NOPASSWD: ALL
You can put it back at the end by removing "NOPASSWD:" from again.
Recommended you run with -Verbose the first time
#>
[Alias("Update-WslResolv")]
[CmdletBinding()]
param(
# DNS Servers you want to use in WSL (by default copied from your local DNS client settings)
[ValidateNotNullOrEmpty()]
[string[]]$DnsServers = $(Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -Expand ServerAddresses -Unique),
# DNS Suffixes you want to search in WSL (by default copied from your local DNS client settings)
[ValidateNotNullOrEmpty()]
[string[]]$DnsSuffixes = $((Get-DnsClientGlobalSetting).SuffixSearchList),
# The Distribution to configure (by default, all of them)
# Be careful when calculating the values for this:
# There is current a bug in wsl that causes it to output nulls after every character
[ValidateNotNullOrEmpty()]
[string[]]$Distribution = $(wsle --list --quiet)
)
foreach ($distro in $Distribution) {
Write-Verbose "Update /etc/resolv.conf for $distro"
wsl --distribution $distro -u root sh -c "unlink /etc/resolv.conf"
Set-WslContent -Distribution $Distro -Path /etc/resolv.conf @(
if ($DnsSuffixes) { "search $($DnsSuffixes -join ' ')" }
$DnsServers | ForEach-Object { "nameserver $_" }
"nameserver 8.8.8.8"
)
}
}
function Disable-WslGenerateResolvConf {
<#
.SYNOPSIS
Update wsl.conf to disable generateResolvConf
#>
[CmdletBinding()]
param(
# The Distribution to configure (by default, all of them)
# Be careful when calculating the values for this:
# There is current a bug in wsl that causes it to output nulls after every character
[ValidateNotNullOrEmpty()]
[string[]]$Distribution = $(wsle --list --quiet),
# Force a restart of the distro(s)
[switch]$Restart
)
foreach ($distro in $Distribution) {
Write-Verbose "Update /etc/wsl.conf for $distro"
#$wslConfPath = "\\wsl$\$distro\etc\wsl.conf"
$wslConfPath = "/etc/wsl.conf"
# Get the current content, if there is any
if (($wsl = wsl --distribution $distro sh -c "cat $wslConfPath" | ConvertFrom-IniContent)) {
if (!($wsl["network"])) {
$wsl["network"] = [ordered]@{}
}
$wsl["network"]["generateResolvConf"] = "false"
} else {
$wsl = [ordered]@{
network = [ordered]@{
"generateResolvConf" = "false"
}
}
}
Set-WslContent -Distribution $Distro -Path $wslConfPath -Content ($wsl | ConvertTo-IniContent)
if ($Restart) {
# See The 8 Second Rule: https://docs.microsoft.com/en-us/windows/wsl/wsl-config#the-8-second-rule
wsl --terminate $distro
# There's a bug in wsl, it's outputting nulls after every character
while ((wsle --list --running --quiet) -eq $distro) {
Start-Sleep -Milliseconds 50
}
# start it back up, hopefully it'll loose the /etc/resolv.conf
wsl --distribution $distro echo hello
}
}
}
function Invoke-Wsl {
<#
.SYNOPSIS
Wrap wsl.exe with Console.Encoding because it ignores Console.Encoding
This way when we get UTF-16 encoding the console handles it.
#>
[Console]::OutputEncoding, $Encoding = [Text.Encoding]::Unicode, [Console]::OutputEncoding
wsl.exe @args
[Console]::OutputEncoding = $Encoding
}
Set-Alias wsle Invoke-Wsl
$Commands = Get-Command *-Wsl* -Module WslHelper -ParameterName Distribution
Register-ArgumentCompleter -CommandName $Commands.Name -ParameterName Distribution -ScriptBlock {
param($CommandName, $ParameterName, $WordToComplete, $CommandLineAst, $PartialBoundParameters)
wsle --list --quiet | ForEach-Object {
[Management.Automation.CompletionResult]::new("'$($_)'", $_, "ParameterValue", $_)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment