-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f8eaf10
Showing
6 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Visual Studio Code | ||
.vscode/* | ||
|
||
# Binaries for programs and plugins | ||
*.exe | ||
*.exe~ | ||
*.dll | ||
*.so | ||
*.dylib | ||
|
||
# Test binary, built with `go test -c` | ||
*.test | ||
|
||
# Output of the go coverage tool, specifically when used with LiteIDE | ||
*.out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Copyright 2019 Ben Pye and contributors | ||
|
||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
|
||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
|
||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# wsl-ssh-pageant | ||
|
||
## Why | ||
I use a Yubikey to store a GPG key pair and I like to use this key pair as my SSH key too. GPG on Windows exposes a Pageant style SSH agent and I wanted a way to use this key within WSL. I have rewritten this in Go as it means the release is a single simple binary, and I like Go. | ||
|
||
## How to use with WSL | ||
|
||
1. On the Windows side start Pageant (or compatible agent such as gpg4win). | ||
|
||
2. Run `wsl-ssh-pageant.exe --wsl C:\wsl-ssh-pageant\ssh-agent.sock` (or any other path, max ~100 characters) | ||
|
||
3. In WSL export the `SSH_AUTH_SOCK` environment variable to point at the socket, for example, if you have `ssh-agent.sock` in `C:\wsl-ssh-pageant` | ||
``` | ||
$ export SSH_AUTH_SOCK=/mnt/c/wsl-ssh-pageant/ssh-agent.sock | ||
``` | ||
|
||
4. The SSH keys from Pageant should now be usable by `ssh` | ||
|
||
## How to use with Windows 10 native OpenSSH client | ||
|
||
1. On the Windows side start Pageant (or compatible agent such as gpg4win). | ||
|
||
2. Run `wsl-ssh-pageant.exe --winssh ssh-pageant` (or any other name) | ||
|
||
3. In `cmd` export the `SSH_AUTH_SOCK` environment variable or define it in your Environment Variables on Windows. Use the name you gave the pipe, for example: | ||
|
||
``` | ||
$ set SSH_AUTH_SOCK=\\.\pipe\ssh-pageant | ||
``` | ||
|
||
4. The SSH keys from Pageant should now be usable by the native Windows SSH client, try using `ssh` in `cmd.exe` | ||
|
||
## Note | ||
|
||
You can use both `--winssh` and `--wsl` parameters at the same time with the same process to proxy for both | ||
|
||
# Frequently asked questions | ||
|
||
## How do I download it? | ||
Grab the latest release on the [releases page](https://github.com/benpye/wsl-ssh-pageant/releases). | ||
|
||
## How do I build this? | ||
For WSL support you will need Go 1.12 or later,. Go 1.12 added support for `AF_UNIX` sockets on Windows. | ||
|
||
## What version of Windows do I need? | ||
You need Windows 10 1803 or later for WSL support as it is the first version supporting `AF_UNIX` sockets. You can still use this with the native [Windows SSH client](https://github.com/PowerShell/Win32-OpenSSH/releases) on earlier builds. | ||
|
||
## You didn't answer my question! | ||
Please open an issue, I do try and keep on top of them, promise. | ||
|
||
# Credit | ||
|
||
* Thanks to [John Starks](https://github.com/jstarks/) for [npiperelay](https://github.com/jstarks/npiperelay/) for an example of a more secure way to create a stream between WSL and Linux before `AF_UNIX` sockets were available. | ||
* Thanks for [Mark Dietzer](https://github.com/Doridian) for several contributions to the old .NET implementation. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module github.com/benpye/wsl-ssh-pageant | ||
|
||
go 1.12 | ||
|
||
require ( | ||
github.com/Microsoft/go-winio v0.4.11 | ||
github.com/lxn/win v0.0.0-20181015143721-a7f87360b10e | ||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q= | ||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= | ||
github.com/lxn/win v0.0.0-20181015143721-a7f87360b10e h1:dz4TzIsrPe4XtUyhLkOLdCS8UkVwJKQu4WY8VcIwo3I= | ||
github.com/lxn/win v0.0.0-20181015143721-a7f87360b10e/go.mod h1:jACzEp9RV7NhfPJQkiCNTteU4nkZZVlvkNpYtVOZPfE= | ||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 h1:FDfvYgoVsA7TTZSbgiqjAbfPbK47CNHdWl3h/PJtii0= | ||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"encoding/binary" | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"io" | ||
"log" | ||
"net" | ||
"os" | ||
"os/signal" | ||
"reflect" | ||
"sync" | ||
"syscall" | ||
"unsafe" | ||
|
||
"github.com/Microsoft/go-winio" | ||
"github.com/lxn/win" | ||
"golang.org/x/sys/windows" | ||
) | ||
|
||
var ( | ||
unixSocket = flag.String("wsl", "", "Path to Unix socket for passthrough to WSL") | ||
namedPipe = flag.String("winssh", "", "Named pipe for use with Win32 OpenSSH") | ||
verbose = flag.Bool("verbose", false, "Enable verbose logging") | ||
) | ||
|
||
const ( | ||
// Windows constats | ||
invalidHandleValue = ^windows.Handle(0) | ||
pageReadWrite = 0x4 | ||
fileMapWrite = 0x2 | ||
|
||
// ssh-agent/Pageant constants | ||
agentMaxMessageLength = 8192 | ||
agentCopyDataID = 0x804e50ba | ||
) | ||
|
||
// copyDataStruct is used to pass data in the WM_COPYDATA message. | ||
// We directly pass a pointer to our copyDataStruct type, we need to be | ||
// careful that it matches the Windows type exactly | ||
type copyDataStruct struct { | ||
dwData uintptr | ||
cbData uint32 | ||
lpData uintptr | ||
} | ||
|
||
var queryPageantMutex sync.Mutex | ||
|
||
func queryPageant(buf []byte) (result []byte, err error) { | ||
if len(buf) > agentMaxMessageLength { | ||
err = errors.New("Message too long") | ||
return | ||
} | ||
|
||
hwnd := win.FindWindow(syscall.StringToUTF16Ptr("Pageant"), syscall.StringToUTF16Ptr("Pageant")) | ||
|
||
if hwnd == 0 { | ||
err = errors.New("Could not find Pageant window") | ||
return | ||
} | ||
|
||
// Typically you'd add thread ID here but thread ID isn't useful in Go | ||
// We would need goroutine ID but Go hides this and provides no good way of | ||
// accessing it, instead we serialise calls to queryPageant and treat it | ||
// as not being goroutine safe | ||
mapName := fmt.Sprintf("WSLPageantRequest") | ||
queryPageantMutex.Lock() | ||
|
||
fileMap, err := windows.CreateFileMapping(invalidHandleValue, nil, pageReadWrite, 0, agentMaxMessageLength, syscall.StringToUTF16Ptr(mapName)) | ||
if err != nil { | ||
queryPageantMutex.Unlock() | ||
return | ||
} | ||
defer func() { | ||
windows.CloseHandle(fileMap) | ||
queryPageantMutex.Unlock() | ||
}() | ||
|
||
sharedMemory, err := windows.MapViewOfFile(fileMap, fileMapWrite, 0, 0, 0) | ||
if err != nil { | ||
return | ||
} | ||
defer windows.UnmapViewOfFile(sharedMemory) | ||
|
||
sharedMemoryArray := (*[agentMaxMessageLength]byte)(unsafe.Pointer(sharedMemory)) | ||
copy(sharedMemoryArray[:], buf) | ||
|
||
mapNameWithNul := mapName + "\000" | ||
|
||
// We use our knowledge of Go strings to get the length and pointer to the | ||
// data and the length directly | ||
cds := copyDataStruct{ | ||
dwData: agentCopyDataID, | ||
cbData: uint32(((*reflect.StringHeader)(unsafe.Pointer(&mapNameWithNul))).Len), | ||
lpData: ((*reflect.StringHeader)(unsafe.Pointer(&mapNameWithNul))).Data, | ||
} | ||
|
||
ret := win.SendMessage(hwnd, win.WM_COPYDATA, 0, uintptr(unsafe.Pointer(&cds))) | ||
if ret == 0 { | ||
err = errors.New("WM_COPYDATA failed") | ||
return | ||
} | ||
|
||
len := binary.BigEndian.Uint32(sharedMemoryArray[:4]) | ||
len += 4 | ||
|
||
if len > agentMaxMessageLength { | ||
err = errors.New("Return message too long") | ||
return | ||
} | ||
|
||
result = make([]byte, len) | ||
copy(result, sharedMemoryArray[:len]) | ||
|
||
return | ||
} | ||
|
||
var failureMessage = [...]byte{0, 0, 0, 1, 5} | ||
|
||
func handleConnection(conn net.Conn) { | ||
defer conn.Close() | ||
|
||
reader := bufio.NewReader(conn) | ||
|
||
for { | ||
lenBuf := make([]byte, 4) | ||
_, err := io.ReadFull(reader, lenBuf) | ||
if err != nil { | ||
if *verbose { | ||
log.Printf("io.ReadFull error '%s'", err) | ||
} | ||
return | ||
} | ||
|
||
len := binary.BigEndian.Uint32(lenBuf) | ||
buf := make([]byte, len) | ||
_, err = io.ReadFull(reader, buf) | ||
if err != nil { | ||
if *verbose { | ||
log.Printf("io.ReadFull error '%s'", err) | ||
} | ||
return | ||
} | ||
|
||
result, err := queryPageant(append(lenBuf, buf...)) | ||
if err != nil { | ||
// If for some reason talking to Pageant fails we fall back to | ||
// sending an agent error to the client | ||
if *verbose { | ||
log.Printf("Pageant query error '%s'", err) | ||
} | ||
result = failureMessage[:] | ||
} | ||
|
||
_, err = conn.Write(result) | ||
if err != nil { | ||
if *verbose { | ||
log.Printf("net.Conn.Write error '%s'", err) | ||
} | ||
return | ||
} | ||
} | ||
} | ||
|
||
func listenLoop(ln net.Listener) { | ||
defer ln.Close() | ||
|
||
for { | ||
conn, err := ln.Accept() | ||
if err != nil { | ||
log.Printf("net.Listener.Accept error '%s'", err) | ||
return | ||
} | ||
|
||
if *verbose { | ||
log.Printf("New connection: %v\n", conn) | ||
} | ||
|
||
go handleConnection(conn) | ||
} | ||
} | ||
|
||
func main() { | ||
flag.Parse() | ||
|
||
var unix, pipe net.Listener | ||
var err error | ||
|
||
done := make(chan bool, 1) | ||
|
||
sigs := make(chan os.Signal, 1) | ||
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) | ||
go func() { | ||
sig := <-sigs | ||
switch sig { | ||
case os.Interrupt: | ||
log.Printf("Caught signal") | ||
done <- true | ||
} | ||
}() | ||
|
||
if *unixSocket != "" { | ||
unix, err = net.Listen("unix", *unixSocket) | ||
|
||
if err != nil { | ||
log.Fatalf("Could not open socket %s, error '%s'\n", *unixSocket, err) | ||
} | ||
|
||
defer unix.Close() | ||
log.Printf("Listening on Unix socket: %s\n", *unixSocket) | ||
go func() { | ||
listenLoop(unix) | ||
// If for some reason our listener breaks, kill the program | ||
done <- true | ||
}() | ||
} | ||
|
||
if *namedPipe != "" { | ||
namedPipeFullName := "\\\\.\\pipe\\" + *namedPipe | ||
var cfg = &winio.PipeConfig{} | ||
pipe, err = winio.ListenPipe(namedPipeFullName, cfg) | ||
|
||
if err != nil { | ||
log.Fatalf("Could not open named pipe %s, error '%s'\n", namedPipeFullName, err) | ||
} | ||
|
||
defer pipe.Close() | ||
log.Printf("Listening on named pipe: %s\n", namedPipeFullName) | ||
go func() { | ||
listenLoop(pipe) | ||
// If for some reason our listener breaks, kill the program | ||
done <- true | ||
}() | ||
} | ||
|
||
if *namedPipe == "" && *unixSocket == "" { | ||
flag.PrintDefaults() | ||
os.Exit(1) | ||
} | ||
|
||
// Wait until we are signalled as finished | ||
<-done | ||
|
||
log.Printf("Exiting...") | ||
} |