published on

Setting up to SSH Directly to WSL 2

I have been using a Windows 10 desktop as my daily driver for the awhile now. I can honestly say that the transition would not have been possible with WSL 2. I think in bash (run zsh). Trying to use cmd.exe or powershell just is not in the cards.

On the other hand I’ve lived in the Apple ecosystem for a long time. My work machine is a MacBook Pro. My personal machine is an iPad Pro with blink installed. I often find myself upstairs wanting to work on a bit of code on the desktop or wishing I had 16 cores and 64G ram.

RDP is heavy. I just want a terminal. I am running Ubuntu in WSL 2. Unfortunately, like Ubuntu in a container, WSL 2 doesn’t run a full init system. WSL 2 also, is effectively a VM so a port exposed on the VM isn’t going to be visible on the host’s network interface. I could run OpenSSH in Windows 10 and deal with the differences or login and immedatiate drop to WSL shell. Here’s how to Install and manage SSH Keys.

There must be something that just drops me into a Linux shell? Turns out I spent awhile learning about how WSL 2 works and figured out a solution. Its not easier then running SSH directly in Windows but it checks the boxes for me.

Replace Linux Init with Windows

The trick here is that WSL 2 is capable of running Linux binaries directly. So what we’re going to do is run a script at startup (or login) in Windows that starts the SSH service in Ubuntu. This effectively replaces Ubuntu init system with Windows. The first challenge is by default WSL 2 runs the Linux binaries as an unprivledged user. So we’ll need to use sudo. We want this to be automated so no password prompt so we’re going to need to make some changes to the sudoer file as well.

It looks like this:

powershell.exe "& 'C:\Windows\System32\bash.exe' -c 'sudo /etc/init.d/ssh start'"

Windows Run

Lets go ahead and from the Windows menu start the Run application. We will want to launch the shell:startup command which will open a folder that contains files that will be executed on startup. I copied the powershell line above into sshd.bat.

Now lets go ahead and edit the sudoer’s file with sudo visudo:

%sudo ALL=NOPASSWD: /etc/init.d/ssh start

Normally I’m not a huge fan of NOPASSWD in a sudoers file but in this case, we are limiting it to the ssh init script. Thats fine for my home desktop.

You can test the script by double clicking it, logging out and back in, or restarting your computer. Once that is done you should be able to ssh localhost inside of WSL 2.

Bridge the WSL 2 VM and the Windows Host Interface

So now you have SSH running in the Linux VM but that a separate virtual interface from the Windows host. So you are not quite ready to access SSH from outside of the Windows Host. You will need to do two things.

  1. Use netsh to setup a port proxy forwarding port 22 on the host interface to port 22 on the VM.
  2. Open port 22 on the Windows Firewall.
                 +-----------------------------------------------------+
                 |   Windows Host                                      |
                 |                                                     |
                 |                  +------------------------+         |
                 |                  |   Ubuntu WSL 2         |         |
                 |                  |                        |         |
                 |  netsh portproxy |                        |         |
port 22 <-------->  <-------------> | /etc/init/ssh          |         |
     0.0.0.0     |    172.16.x.x    |                        |         |
                 |                  |                        |         |
                 |                  +------------------------+         |
                 |                                                     |
                 |                                                     |
                 +-----------------------------------------------------

Luckily there is a script for this. My many thanks to Edwin Chiwoma for this script.

$remoteport = bash.exe -c "ifconfig eth0 | grep 'inet '"
$found = $remoteport -match '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';

if( $found ){
  $remoteport = $matches[0];
} else{
  echo "The Script Exited, the ip address of WSL 2 cannot be found";
  exit;
}

#[Ports]
#All the ports you want to forward separated by coma
$ports=@(22);


#[Static ip]
#You can change the addr to your ip config to listen to a specific address
$addr='0.0.0.0';
$ports_a = $ports -join ",";


#Remove Firewall Exception Rules
iex "Remove-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' ";

#adding Exception Rules for inbound and outbound Rules
iex "New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Outbound -LocalPort $ports_a -Action Allow -Protocol TCP";
iex "New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Inbound -LocalPort $ports_a -Action Allow -Protocol TCP";

for( $i = 0; $i -lt $ports.length; $i++ ){
  $port = $ports[$i];
  iex "netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$addr";
  iex "netsh interface portproxy add v4tov4 listenport=$port listenaddress=$addr connectport=$port connectaddress=$remoteport";
}

I suggest you save this script somewhere handy. I run in the fast ring of Windows Insiders and best as I can tell I have to run this script every time Windows is updated.

Hopefully this has been useful for you. Let me know if you have any suggestions on improving it!