-
-
Save kevinvalk/3ccd5b360fd568862b4a397a9df9ed26 to your computer and use it in GitHub Desktop.
#!/usr/bin/python | |
""" | |
msysGit to Unix socket proxy | |
============================ | |
This small script is intended to help use msysGit sockets with the new Windows Linux Subsystem (aka Bash for Windows). | |
It was specifically designed to pass SSH keys from the KeeAgent module of KeePass secret management application to the | |
ssh utility running in the WSL (it only works with Linux sockets). However, my guess is that it will have uses for other | |
applications as well. | |
In order to efficiently use it, I add it at the end of the ~/.bashrc file, like this: | |
export SSH_AUTH_SOCK="/tmp/.ssh-auth-sock" | |
~/bin/msysgit2unix-socket.py /mnt/c/Users/User/keeagent.sock:$SSH_AUTH_SOCK | |
Command line usage: msysgit2unix-socket.py [-h] [--downstream-buffer-size N] | |
[--upstream-buffer-size N] [--listen-backlog N] | |
[--timeout N] [--pidfile FILE] | |
source:destination [source:destination ...] | |
Positional arguments: | |
source:destination A pair of a source msysGit and a destination Unix | |
sockets. | |
Optional arguments: | |
-h, --help show this help message and exit | |
--downstream-buffer-size N | |
Maximum number of bytes to read at a time from the | |
Unix socket. | |
--upstream-buffer-size N | |
Maximum number of bytes to read at a time from the | |
msysGit socket. | |
--listen-backlog N Maximum number of simultaneous connections to the Unix | |
socket. | |
--timeout N Timeout. | |
--pidfile FILE Where to write the PID file. | |
""" | |
import argparse | |
import asyncore | |
import os | |
import re | |
import signal | |
import socket | |
import sys | |
import errno | |
import atexit | |
# NOTE: Taken from http://stackoverflow.com/a/6940314 | |
def PidExists(pid): | |
"""Check whether pid exists in the current process table. | |
UNIX only. | |
""" | |
if pid < 0: | |
return False | |
if pid == 0: | |
# According to "man 2 kill" PID 0 refers to every process | |
# in the process group of the calling process. | |
# On certain systems 0 is a valid PID but we have no way | |
# to know that in a portable fashion. | |
raise ValueError('invalid PID 0') | |
try: | |
os.kill(pid, 0) | |
except OSError as err: | |
if err.errno == errno.ESRCH: | |
# ESRCH == No such process | |
return False | |
elif err.errno == errno.EPERM: | |
# EPERM clearly means there's a process to deny access to | |
return True | |
else: | |
# According to "man 2 kill" possible error values are | |
# (EINVAL, EPERM, ESRCH) | |
raise | |
else: | |
return True | |
class UpstreamHandler(asyncore.dispatcher_with_send): | |
""" | |
This class handles the connection to the TCP socket listening on localhost that makes the msysGit socket. | |
""" | |
def __init__(self, downstream_dispatcher, upstream_path): | |
asyncore.dispatcher.__init__(self) | |
self.out_buffer = b'' | |
self.downstream_dispatcher = downstream_dispatcher | |
self.create_socket(socket.AF_INET, socket.SOCK_STREAM) | |
self.connect((b'localhost', UpstreamHandler.load_tcp_port_from_msysgit_socket_file(upstream_path))) | |
@staticmethod | |
def load_tcp_port_from_msysgit_socket_file(path): | |
with open(path, 'r') as f: | |
m = re.search(b'>([0-9]+)', f.readline()) | |
return int(m.group(1)) | |
def handle_connect(self): | |
pass | |
def handle_close(self): | |
self.close() | |
self.downstream_dispatcher.close() | |
def handle_read(self): | |
data = self.recv(config.upstream_buffer_size) | |
if data: | |
self.downstream_dispatcher.send(data) | |
class DownstreamHandler(asyncore.dispatcher_with_send): | |
""" | |
This class handles the connections that are being accepted on the Unix socket. | |
""" | |
def __init__(self, downstream_socket, upstream_path): | |
asyncore.dispatcher.__init__(self, downstream_socket) | |
self.out_buffer = b'' | |
self.upstream_dispatcher = UpstreamHandler(self, upstream_path) | |
def handle_close(self): | |
self.close() | |
self.upstream_dispatcher.close() | |
def handle_read(self): | |
data = self.recv(config.downstream_buffer_size) | |
if data: | |
self.upstream_dispatcher.send(data) | |
class MSysGit2UnixSocketServer(asyncore.dispatcher): | |
""" | |
This is the "server" listening for connections on the Unix socket. | |
""" | |
def __init__(self, upstream_socket_path, unix_socket_path): | |
asyncore.dispatcher.__init__(self) | |
self.upstream_socket_path = upstream_socket_path | |
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
self.bind(unix_socket_path) | |
self.listen(config.listen_backlog) | |
def handle_accept(self): | |
pair = self.accept() | |
if pair is not None: | |
sock, addr = pair | |
DownstreamHandler(sock, self.upstream_socket_path) | |
def build_config(): | |
class ProxyAction(argparse.Action): | |
def __call__(self, parser, namespace, values, option_string=None): | |
proxies = [] | |
for value in values: | |
src_dst = value.partition(':') | |
if src_dst[1] == b'': | |
raise parser.error(b'Unable to parse sockets proxy pair "%s".' % value) | |
proxies.append([src_dst[0], src_dst[2]]) | |
setattr(namespace, self.dest, proxies) | |
parser = argparse.ArgumentParser( | |
description='Transforms msysGit compatible sockets to Unix sockets for the Windows Linux Subsystem.') | |
parser.add_argument(b'--downstream-buffer-size', default=8192, type=int, metavar=b'N', | |
help=b'Maximum number of bytes to read at a time from the Unix socket.') | |
parser.add_argument(b'--upstream-buffer-size', default=8192, type=int, metavar=b'N', | |
help=b'Maximum number of bytes to read at a time from the msysGit socket.') | |
parser.add_argument(b'--listen-backlog', default=100, type=int, metavar=b'N', | |
help=b'Maximum number of simultaneous connections to the Unix socket.') | |
parser.add_argument(b'--timeout', default=60, type=int, help=b'Timeout.', metavar=b'N') | |
parser.add_argument(b'--pidfile', default=b'/tmp/msysgit2unix-socket.pid', metavar=b'FILE', | |
help=b'Where to write the PID file.') | |
parser.add_argument(b'proxies', nargs=b'+', action=ProxyAction, metavar='source:destination', | |
help=b'A pair of a source msysGit and a destination Unix sockets.') | |
return parser.parse_args() | |
def daemonize(): | |
try: | |
pid = os.fork() | |
if pid > 0: | |
sys.exit() | |
except OSError: | |
sys.stderr.write(b'Fork #1 failed.') | |
sys.exit(1) | |
os.chdir(b'/') | |
os.setsid() | |
os.umask(0) | |
try: | |
pid = os.fork() | |
if pid > 0: | |
sys.exit() | |
except OSError: | |
sys.stderr.write(b'Fork #2 failed.') | |
sys.exit(1) | |
sys.stdout.flush() | |
sys.stderr.flush() | |
si = open('/dev/null', 'r') | |
so = open('/dev/null', 'a+') | |
se = open('/dev/null', 'a+') | |
os.dup2(si.fileno(), sys.stdin.fileno()) | |
os.dup2(so.fileno(), sys.stdout.fileno()) | |
os.dup2(se.fileno(), sys.stderr.fileno()) | |
pid = str(os.getpid()) | |
with open(config.pidfile, 'w+') as f: | |
f.write(b'%s\n' % pid) | |
def cleanup(): | |
try: | |
for pair in config.proxies: | |
if os.path.exists(pair[1]): | |
os.remove(pair[1]) | |
if os.path.exists(config.pidfile): | |
os.remove(config.pidfile) | |
except Exception as e: | |
sys.stderr.write(b'%s' % (e)) | |
if __name__ == b'__main__': | |
config = build_config() | |
if os.path.exists(config.pidfile): | |
# Check if process is really running, if not run cleanup | |
f = open(config.pidfile, 'r') | |
if PidExists(int(f.readline().strip())): | |
sys.stderr.write(b'%s: Already running (or at least pidfile "%s" already exists).\n' % (sys.argv[0], config.pidfile)) | |
sys.exit(0) | |
else: | |
cleanup() | |
for pair in config.proxies: | |
MSysGit2UnixSocketServer(pair[0], pair[1]) | |
daemonize() | |
# Redundant cleanup :) | |
atexit.register(cleanup) | |
signal.signal(signal.SIGINT, cleanup) | |
signal.signal(signal.SIGTERM, cleanup) | |
asyncore.loop(config.timeout, True) |
@FlorinAsavoaie @kevinvalk - I fixed problems with strings incorrectly marked as bytes. See my fork: https://gist.github.com/duebbert/4298b5f4eb7cc064b09e9d865dd490c9
@duebbert, @kevenvalk: I get a notice each time I open an extra console:
/home/build/bin/msysgit2unix-socket.py: Already running (or at least pidfile "/tmp/msysgit2unix-socket.pid" already exists).
I think this notice may be helpful, but in this case (on startup) it is rather annoying.
@kevinvalk this should use either /usr/bin/python2
or /usr/bin/env python2
I tried to use this script in WSL2 and got an error when trying to read keys. I have WSL and WSL2 with the same configurations.
WSL
prart of output strace ssh-add -l
socket(AF_UNIX, SOCK_STREAM, 0) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
connect(3, {sa_family=AF_UNIX, sun_path="/tmp/.ssh-auth-sock"}, 110) = 0
write(3, "\0\0\0\1", 4) = 4
write(3, "\v", 1) = 1
read(3, "\0\0\6\241", 4) = 4
read(3, "\f\0\0\0\4\0\0\2\27\0\0\0\7ssh-rsa\0\0\0\3\1\0\1\0\0\2\1\0"..., 1024) = 1024
read(3, "\216\213\305HQ\226\241\177,O\374\315\333\33\302\273\341\272\2526\34\3\260\263g\327\17\330\25\17\360\36"..., 673) = 673
fstat(1, {st_mode=S_IFCHR|0660, st_rdev=makedev(4, 3), ...}) = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(1, "4096 SHA256:ks4Ep+nc/QHwE0iWtVbE"..., 694096 SHA256:ks4Ep+nc/QHwE0iWtBbEexmECco2rO06W3dIWlndtgU id_rsa (RSA)
) = 69
write(1, "4096 SHA256:KBU39i4Ca+4AQo7yviJt"..., 734096 SHA256:KBU39i4Ca+4AQo7yvTJtlNm/Iyxv/BaqHAI7lP0z6js ..tfat1 (RSA)
) = 73
close(3) = 0
exit_group(0) = ?
+++ exited with 0 +++
WSL2
strace ssh-add -l
socket(AF_UNIX, SOCK_STREAM, 0) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
connect(3, {sa_family=AF_UNIX, sun_path="/tmp/.ssh-auth-sock"}, 110) = 0
write(3, "\0\0\0\1", 4) = 4
write(3, "\v", 1) = 1
read(3, "", 4) = 0
write(2, "error fetching identities: commu"..., 59error fetching identities: communication with agent failed
) = 59
close(3) = 0
exit_group(1) = ?
+++ exited with 1 +++
You might find this Ansible role to install and configure the script useful: https://github.com/ColOfAbRiX/role-wsl-keeagent
I also made few changes to the script (original script linked) https://github.com/ColOfAbRiX/role-wsl-keeagent/blob/master/files/msysgit2unix-socket.py and added a SystemV init script https://github.com/ColOfAbRiX/role-wsl-keeagent/blob/master/templates/wsl-keeagent.j2
Any guide on how to get WSL2 working with KeeAgent?
Update to use pthon3 ? Python2 no longer included in ubuntu
Update to use pthon3 ? Python2 no longer included in ubuntu
Please try this fork: https://gist.github.com/fktpp/ae4c93240890a1488c75e14f32ebc532
Thanks @FlorinAsavoaie @kevinvalk - this is great!