├── README.md └── hvnc.ps1 /README.md: -------------------------------------------------------------------------------- 1 | # Poor man's HyperV netcat 2 | 3 | Simple PowerShell script to create a TCP-to-HyperV VMBus socket proxy. 4 | The main usecase is to ssh to a Linux HyperV VM from Windows through HyperV VMBus socket. 5 | 6 | ## TL;DR 7 | 8 | On the Linux VM, use `socat` to forward VMBus socket to sshd TCP socket: 9 | 10 | socat -b65536 -lm SOCKET-LISTEN:40:0:x0000x16000000xffffffffx00000000,reuseaddr,fork TCP:localhost:22 11 | 12 | On the Windows host, run this script in the background (do not forget to update the 1st line with the name of the VM you want to connect to): 13 | 14 | powershell.exe -command 'start-process -windowstyle hidden -verb runas "powershell.exe" -argumentlist "-executionpolicy remotesigned -F hvnc.ps1 -VM "' 15 | 16 | You can now ssh to your guest Linux VM from Windows by connecting to localhost:2222: 17 | 18 | ssh -p2222 user@locahost 19 | 20 | ## Why? 21 | 22 | HyperV and WSL are an interesting platform for Linux development in a corporate environment (understand: IT supports Windows only), but there are some rough edges. 23 | The main issue I had to solve was to consistently access my VM through SSH from WSL. 24 | My corporate VPN client (that I have to use when not in the office) breaks all networking (hence breaking ssh-to-VM when using the VPN) and HyperV requires Windows admin rights (which you cannot get from WSL). 25 | The VMBus socket is not impacted by the VPN network configuration and I can ssh to localhost from WSL through TCP. 26 | 27 | ## Performance 28 | 29 | It seems that performance is higher than plain TCP over hvnet (HyperV para-virtualized networking interface): my simple non-representative benchmark gave me ~30MB/s using TCP-VMBus-TCP vs ~20MB/s for plain TCP (maybe thanks to higher MTU?). 30 | -------------------------------------------------------------------------------- /hvnc.ps1: -------------------------------------------------------------------------------- 1 | # HyperV VM name 2 | # you can get them using eg. 'hvc list' 3 | param([string]$VM="*") 4 | 5 | # HyperV socket endpoint 6 | # https://docs.microsoft.com/en-us/dotnet/api/system.net.endpoint?view=netframework-4.7.2 7 | # https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/remoting/common/RemoteSessionHyperVSocket.cs 8 | class HyperVEndPoint : System.Net.EndPoint 9 | { 10 | $vmId 11 | $serviceId 12 | 13 | HyperVEndPoint($vmId, $serviceId) 14 | { 15 | $this.vmId = $vmId 16 | $this.serviceId = $serviceId 17 | } 18 | 19 | [System.Net.SocketAddress] Serialize() 20 | { 21 | $saddr = [System.Net.SocketAddress]::new(34, 36); # AF_HYPERV, 36-bytes 22 | $vid = $this.vmId.ToByteArray() 23 | $sid = $this.serviceId.ToByteArray() 24 | $saddr[2] = 0 # reserved1 25 | $saddr[3] = 0 # reserved1 26 | for ($i=0; $i -lt 16; $i++) { 27 | $saddr[$i+ 4] = $vid[$i] # copy VM id 28 | $saddr[$i+20] = $sid[$i] # copy service id 29 | } 30 | return $saddr 31 | } 32 | } 33 | 34 | class SocketPair : System.Net.Sockets.Socket 35 | { 36 | $buffer 37 | $peer 38 | 39 | SocketPair($socket) : base($socket.DuplicateAndClose([System.Diagnostics.Process]::GetCurrentProcess().Id)) {} 40 | SocketPair($family, $type, $protocol) : base($family, $type, $protocol) {} 41 | 42 | static [Array] Pair($s1, $s2, $buffer) 43 | { 44 | $s1.buffer = $s2.buffer = $buffer 45 | $s1.peer = $s2 46 | $s2.peer = $s1 47 | return $s1, $s2 48 | } 49 | 50 | [bool] ProcessEvent() 51 | { 52 | $len = $this.Receive($this.buffer) 53 | if ($len -le 0) { 54 | return 0 55 | } 56 | $len2 = $this.peer.Send($this.buffer, $len, 0) 57 | return ($len2 -eq $len) 58 | } 59 | } 60 | 61 | class SocketServer : System.Net.Sockets.Socket 62 | { 63 | $buf 64 | $slist 65 | $vep 66 | 67 | SocketServer($socket) : base($socket) {} 68 | 69 | static [SocketServer] Create($tep, $vep) 70 | { 71 | $s = [System.Net.Sockets.Socket]::new($tep.AddressFamily, 72 | [System.Net.Sockets.SocketType]::Stream, 73 | [System.Net.Sockets.ProtocolType]::Tcp) 74 | $s.Bind($tep) 75 | $s.Listen(32) 76 | $s = [SocketServer]::new($s.DuplicateAndClose([System.Diagnostics.Process]::GetCurrentProcess().Id)) 77 | $s.buf = New-Object byte[] 65536 78 | $s.vep = $vep 79 | $s.slist = [System.Collections.ArrayList]::new() 80 | $s.slist.Add($s) 81 | return $s 82 | } 83 | 84 | [bool] ProcessEvent() 85 | { 86 | $ts = [SocketPair]::new($this.Accept()) 87 | $vs = [SocketPair]::new(34, 1, 1) # AF_HYPERV, SOCK_STREAM, HV_PROTOCOL_RAW 88 | $vs.Connect($this.vep) 89 | $ts, $vs = [SocketPair]::Pair($ts, $vs, $this.buf) 90 | $this.slist.Add($ts) 91 | $this.slist.Add($vs) 92 | return 1 93 | } 94 | 95 | [void] Run() 96 | { 97 | while (1) { 98 | $rlist = $this.slist.Clone() 99 | [System.Net.Sockets.Socket]::Select($rlist, $null, $null, -1) 100 | foreach ($s in $rlist) { 101 | if (!$s.ProcessEvent()) { 102 | $this.slist.Remove($s) 103 | $this.slist.Remove($s.peer) 104 | $s.Close() 105 | $s.peer.Close() 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | # 113 | # Connect to the VM vsock 114 | # On Linux you can use eg. sudo socat SOCKET-LISTEN:40:0:x0000x16000000xffffffffx00000000,reuseaddr,fork TCP:localhost:22 115 | # to redirect to local ssh 116 | # 117 | # get VM GUID from name 118 | $vid = [GUID](Get-VM -Name $VM).Id 119 | # Service GUID 120 | # See https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service 121 | # We use port 22 here (0x16) 122 | $sid=[GUID]"00000016-facb-11e6-bd58-64006a7986d3" 123 | $vep = [HyperVEndPoint]::new($vid, $sid) 124 | $tep = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Parse("127.0.0.1"), 2222) 125 | [SocketServer]::Create($tep, $vep).Run() 126 | --------------------------------------------------------------------------------