|
#!/usr/bin/env python3 |
|
import asyncio |
|
import collections |
|
import os |
|
import random |
|
import re |
|
import shlex |
|
import subprocess |
|
import sys |
|
|
|
|
|
STUN_SERVER = 'stun.l.google.com' |
|
STUN_PORT = '19302' |
|
LOCAL_ADDRESS_RE = re.compile(r'Local address: (?P<ip>[\d.]+):(?P<port>\d+)') |
|
MAPPED_ADDRESS_RE = re.compile(r'Mapped address: (?P<ip>[\d.]+):(?P<port>\d+)') |
|
MOSH_KEY_RE = re.compile(r'MOSH CONNECT \d+ (?P<key>.+)') |
|
|
|
Address = collections.namedtuple( |
|
'Address', |
|
['ip', 'port', 'mapped_ip', 'mapped_port'], |
|
) |
|
|
|
|
|
def parse_stun(stdout): |
|
local_match = LOCAL_ADDRESS_RE.search(stdout) |
|
mapped_match = MAPPED_ADDRESS_RE.search(stdout) |
|
if not local_match: |
|
raise RuntimeError('Cannot find local address and port,', stdout) |
|
if not mapped_match: |
|
raise RuntimeError('Cannot find mapped address and port,', stdout) |
|
|
|
address = Address( |
|
local_match.group('ip'), |
|
local_match.group('port'), |
|
mapped_match.group('ip'), |
|
mapped_match.group('port'), |
|
) |
|
return address |
|
|
|
|
|
async def find_local_mapping(): |
|
proc = await asyncio.create_subprocess_exec( |
|
'stunclient', |
|
STUN_SERVER, |
|
STUN_PORT, |
|
stdin=subprocess.DEVNULL, |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE, |
|
) |
|
stdout, stderr = await proc.communicate() |
|
if proc.returncode != 0: |
|
raise RuntimeError('Failed to run stunclient locally,', stderr) |
|
|
|
return parse_stun(stdout.decode()) |
|
|
|
|
|
async def find_remote_mapping(target): |
|
remote_cmd = shlex.join(['stunclient', STUN_SERVER, STUN_PORT]) |
|
proc = await asyncio.create_subprocess_exec( |
|
'ssh', |
|
'-v', |
|
target, |
|
remote_cmd, |
|
stdin=subprocess.DEVNULL, |
|
stdout=subprocess.PIPE, |
|
stderr=None, |
|
) |
|
stdout, stderr = await proc.communicate() |
|
if proc.returncode != 0: |
|
raise RuntimeError('Failed to run stunclient remotely at', target) |
|
|
|
return parse_stun(stdout.decode()) |
|
|
|
|
|
async def punch_local(local_port, remote_mapped_ip, remote_mapped_port): |
|
proc = await asyncio.create_subprocess_exec( |
|
'udp_hole_punch', |
|
local_port, |
|
remote_mapped_ip, |
|
remote_mapped_port, |
|
stdin=subprocess.DEVNULL, |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE, |
|
) |
|
stdout, stderr = await proc.communicate() |
|
if proc.returncode != 0: |
|
raise RuntimeError('Failed to run udp_hole_punch locally,', stderr) |
|
|
|
|
|
async def punch_remote(target, remote_port, local_mapped_ip, local_mapped_port): |
|
remote_cmd = shlex.join([ |
|
'udp_hole_punch', |
|
remote_port, |
|
local_mapped_ip, |
|
local_mapped_port, |
|
]) |
|
proc = await asyncio.create_subprocess_exec( |
|
'ssh', |
|
target, |
|
remote_cmd, |
|
stdin=subprocess.DEVNULL, |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE, |
|
) |
|
stdout, stderr = await proc.communicate() |
|
if proc.returncode != 0: |
|
raise RuntimeError('Failed to run udp_hole_punch remotely,', stderr) |
|
|
|
|
|
async def start_server(target, port): |
|
remote_cmd = shlex.join([ |
|
'mosh-server', |
|
'new', |
|
'-p', |
|
port, |
|
]) |
|
proc = await asyncio.create_subprocess_exec( |
|
'ssh', |
|
target, |
|
remote_cmd, |
|
stdin=subprocess.DEVNULL, |
|
stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE, |
|
) |
|
stdout, stderr = await proc.communicate() |
|
if proc.returncode != 0: |
|
raise RuntimeError('Failed to start mosh-server remotely,', stderr) |
|
|
|
match = MOSH_KEY_RE.search(stdout.decode()) |
|
if not match: |
|
raise RuntimeError('Failed to extract mosh-server key', stdout) |
|
|
|
return match.group('key') |
|
|
|
|
|
async def setup(target): |
|
local, remote = await asyncio.gather( |
|
find_local_mapping(), |
|
find_remote_mapping(target), |
|
) |
|
await asyncio.gather( |
|
punch_local(local.port, remote.mapped_ip, remote.mapped_port), |
|
punch_remote(target, remote.port, local.mapped_ip, local.mapped_port), |
|
) |
|
print('UDP hole punched', local, remote) |
|
|
|
key = await start_server(target, remote.port) |
|
return local.port, key, remote.mapped_ip, remote.mapped_port |
|
|
|
|
|
def main(target): |
|
local_port, key, server_ip, server_port = asyncio.run(setup(target)) |
|
os.putenv('MNB_PORT', local_port) |
|
os.putenv('LD_PRELOAD', os.path.join(os.environ['HOME'], 'bin', 'mnb.so')) |
|
os.putenv('MOSH_KEY', key) |
|
print(local_port, key, server_ip, server_port) |
|
os.execvp( |
|
'mosh-client', |
|
['mosh-client', server_ip, server_port], |
|
) |
|
|
|
|
|
if __name__ == '__main__': |
|
main(sys.argv[1]) |