Skip to content

Instantly share code, notes, and snippets.

@zjiekai
Last active January 20, 2016 06:24
Show Gist options
  • Save zjiekai/3755d9b22824bb3e1012 to your computer and use it in GitHub Desktop.
Save zjiekai/3755d9b22824bb3e1012 to your computer and use it in GitHub Desktop.
#!/usr/bin/python
#
# This is the relay script mentioned in http://blog.zorinaq.com/?e=81
#
# Listens on the address and port specified by --local-ip and --local-port, and
# relay all connections to the endpoint specified by --remote-hosts and
# --remote-port. Multiple remote hosts can be specified: one will be selected
# randomly for each connection.
#
# Optionally, if --mode 1:<secret> is specified, insert the secret key as the
# first bytes of data transmitted through each relayed connection, and if
# --mode 2:<secret> is specified, verify and remove the secret key (ignore
# the connection by discarding all data if the key does not match).
#
# I recommend a long hex string for the secret, for example:
# $ secret=`ps aux | md5sum | cut -c 1-32`
# $ ./tcprelay-secret-exp.py [...] -m 1:"$secret"
#import asyncake
import asyncore
import socket, random, struct
import re
class forwarder(asyncore.dispatcher):
def __init__(self, ip, port, remoteip, remoteport, mode, secret, backlog=600):
asyncore.dispatcher.__init__(self)
self.remoteip=remoteip
self.remoteport=remoteport
self.mode=mode
self.secret=secret
self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((ip,port))
self.listen(backlog)
def handle_accept(self):
conn, addr = self.accept()
# print '--- Connect --- '
sender(receiver(conn, addr[0], self.mode, self.secret),self.remoteip,self.remoteport, addr[0])
class receiver(asyncore.dispatcher):
def __init__(self, conn, client_ip, mode, secret):
asyncore.dispatcher.__init__(self,conn)
self.mode=mode
self.secret=secret
self.from_remote_buffer=''
self.to_remote_buffer=''
self.sender=None
self.client_ip = client_ip
self.zero_bytes_forwarded = True
# for framing
self.look_for_type = True
self.field_type = None
self.look_for_len_byte_nr = None
self.field_len = None
self.bytes_left_to_extract = None
def handle_connect(self):
pass
def detect_and_remove_framing(self, read):
processed_read = ''
for b in read:
if self.look_for_type:
self.field_type = b
self.look_for_type = False
self.look_for_len_byte_nr = 0
self.field_len = 0
elif self.look_for_len_byte_nr != None:
self.field_len <<= 8
self.field_len += ord(b)
self.look_for_len_byte_nr += 1
if self.look_for_len_byte_nr >= 2:
self.look_for_len_byte_nr = None
self.bytes_left_to_extract = self.field_len
elif self.bytes_left_to_extract != None:
if self.field_type == 'd':
processed_read += b
else:
pass
self.bytes_left_to_extract -= 1
if self.bytes_left_to_extract == 0:
self.bytes_left_to_extract = None
self.look_for_type = True
return processed_read
def handle_read(self):
"""Read from TCP client."""
read = self.recv(4096)
if self.mode == '1': # insert the secret key
# Implement simple framing ('d' for data packets, 'p' for padding packets)
read = 'd' + struct.pack('>h', len(read)) + read
rlen = 0
padding = 1
if padding == 0: # no padding
rlen = 0
elif padding == 1 and len(read) < 1500: # padding
if len(read) < 1000:
rlen = random.randint(1000 - len(read), 1500 - len(read))
else:
rlen = random.randint(0, 1500 - len(read))
if rlen:
read += 'p' + struct.pack('>h', rlen) + ('_' * rlen)
if self.zero_bytes_forwarded:
read = self.secret + read
self.zero_bytes_forwarded = False
elif self.mode == '2': # verify and remove the secret key
if self.zero_bytes_forwarded:
if read.startswith(self.secret):
read = read[len(self.secret):]
self.zero_bytes_forwarded = False
else:
read = ''
read = self.detect_and_remove_framing(read)
# print '%04i -->'%len(read)
self.from_remote_buffer += read
def writable(self):
return (len(self.to_remote_buffer) > 0)
def handle_write(self):
sent = self.send(self.to_remote_buffer)
# print '%04i <--'%sent
self.to_remote_buffer = self.to_remote_buffer[sent:]
def handle_close(self):
self.close()
if self.sender:
self.sender.close()
class sender(asyncore.dispatcher):
def __init__(self, receiver, remoteaddr, remoteport, client_ip):
asyncore.dispatcher.__init__(self)
self.receiver=receiver
receiver.sender=self
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect((random.choice(remoteaddr), remoteport))
def handle_connect(self):
pass
def handle_read(self):
"""Read from TCP server."""
read = self.recv(4096)
# print '<-- %04i'%len(read)
self.receiver.to_remote_buffer += read
def writable(self):
return (len(self.receiver.from_remote_buffer) > 0)
def handle_write(self):
sent = self.send(self.receiver.from_remote_buffer)
# print '--> %04i'%sent
self.receiver.from_remote_buffer = self.receiver.from_remote_buffer[sent:]
def handle_close(self):
# when the buffer has not yet fully been written to the client, don't close quite yet.
# handle_close() will be automatically called again by asyncore
if not self.receiver.to_remote_buffer:
self.close()
self.receiver.close()
if __name__=='__main__':
import optparse
parser = optparse.OptionParser()
parser.add_option(
'-l','--local-ip',
dest='local_ip',default='127.0.0.1',
help='Local IP address to bind to (for listening socket)')
parser.add_option(
'-p','--local-port',
type='int',dest='local_port',
help='Local port to bind to')
parser.add_option(
'-P','--remote-port',
type='int',dest='remote_port',
help='Remote port to connect to')
parser.add_option(
'-r','--remote-hosts',
type='string',dest='remote_hosts',default='127.0.0.1',
help='Remote host(s) to connect to, comma-separated')
parser.add_option(
'-m','--mode',
type='string',dest='mode',
help='Operating mode ("0" for not using a secret key, ' + \
'"1:<secret>" for using/inserting the specified secret key, ' + \
'"2:<secret>" for verifying/stripping the specified secret key')
options, args = parser.parse_args()
alladdresses = {}
for h in options.remote_hosts.split(','):
(name, aliaslist, addresslist) = socket.gethostbyname_ex(h)
for a in addresslist:
alladdresses[a] = None
if options.mode is None:
(mode, secret) = (None, None)
else:
(mode, secret) = options.mode.split(':', 1)
if len(secret) < 32:
raise Exception('secret specified in -m option needs to be at least 32 characters long')
#x = asyncake.AsynCake()
forwarder(options.local_ip, options.local_port, alladdresses.keys(), options.remote_port, mode, secret)
#x.loop()
asyncore.loop()
Hannu: I put it at http://pastebin.com/z3Ygc3jx
Use it like this on the client:
./tcprelay-secret-exp.py -p 5000 -P 5001 -m 1:$secret
And on the server:
./tcprelay-secret-exp.py -p 5003 -P 5004 -m 2:$secret
I did not look into other VPN providers. If you use Chrome, and browse HTTPS websites, and they don’t use weak 1024-bit keys, and they have pinned certificates, and they use TLS 1.2, and you don’t click through cert warnings in case of a MitM attack, the GFW cannot decrypt your traffic.
Anonymous: I did not try that.
mrb, - 15-01-’16 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment