├── .gitignore ├── README.md ├── TODO.txt ├── ca.py ├── docs ├── ca.txt ├── prox.txt └── proxcat.txt ├── modFiltCase.py ├── pkcs12.bat ├── pkcs12.sh ├── prox.py └── proxcat.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TcpProx 2 | ======= 3 | 4 | A small command-line TCP proxy utility written in Python 5 | 6 | Tim Newsham 7 | 29 Oct 2012 8 | 9 | 10 | 11 | Overview 12 | ======= 13 | 14 | This is a small command-line TCP proxy utility written in python. 15 | It is designed to have very minimal requirements - it runs 16 | directly from python (tested in python 2.7) from a single source 17 | file (unless the auto-certificate option is used). When running, 18 | the proxy accepts incoming TCP connections and copies data to a TCP 19 | connection to another machine. Options allow for SSL and IPv6 20 | connections and for the logging of all data. Data is logged in 21 | a format that preserves connection, timing and direction 22 | information and a small utility is provided to dump out the 23 | information in various formats. A small utility is also provided 24 | for generating CA and SSL certificates. This utility is the only 25 | component that relies on an external python library, but it can 26 | be run on a different machine if necessary. 27 | 28 | 29 | QUICKSTART 30 | ======= 31 | 32 | - A normal TCP proxy is straightforward: 33 | - $ ./prox.py -L 8888 www.google.com 80 34 | - connect in another window using curl 35 | or connect to localhost port 80 using some other program 36 | - $ curl http://127.0.0.1:8888/ 37 | 38 | - For SSL, first create and install a CA cert 39 | - $ ./ca.py -c 40 | - $ ./pkcs12.sh ca # if you need a pkcs12 certificate 41 | - take ca.pem or ca.pfx and install it as a root 42 | certificate in your testing browser 43 | 44 | - Run the proxy using an auto-generated certificate: 45 | - modify /etc/hosts to redirect www.test.com to 127.0.0.1 46 | - $ ./prox.py -L 8888 -A www.test.com www.google.com 80 47 | - connect using curl or open the URL in your browser 48 | - $ curl --cacert ca.pem https://www.test.com:8888/ 49 | 50 | 51 | - Or manually generate a certificate (possibly on another machine) 52 | and then run the proxy using that certificate: 53 | - $ ./ca.py www.test.com 54 | - $ ./prox.py -L 8888 www.google.com 80 55 | - $ curl --cacert ca.pem https://www.test.com:8888/ 56 | 57 | - To view data logged to a file by prox.py, use proxcat: 58 | - $ ./proxcat.py -x log.txt 59 | 60 | 61 | DEPENDENCIES 62 | ======= 63 | 64 | TcpProx requires a python interpreter and the M2Crypto package 65 | from http://www.heikkitoivonen.net/m2crypto/. The prox.py 66 | program can be run with only the prox.py file and without the 67 | M2Crypto package installed if the -A option is not used. 68 | 69 | 70 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - error messages are not great. 2 | - should check if the necessary cert files are present before using 3 | them so I can print out a more meaningful error message 4 | 5 | - possibly handle half-closes 6 | 7 | - possibly handle asynchronous ssl handshakes 8 | 9 | - XXX Big bug! 10 | - it looks like the SSL connection proxying is not working properly 11 | - try --ssl-in but not -ssl-out, using netcat as the listener 12 | and openssl s_client as the connector 13 | - at first the netcat's output gets sent 14 | - after the openssl client types something, the netcat side 15 | cant seem to send anything anymore! but its still selecting! 16 | - the openssl sender stuff doesnt get passed on to netcat until 17 | one select later.. 18 | - Tried repro on freebsd. bug was not observed. I tried all 19 | combinations of -sssl-in and -sssl-out and tried text and 20 | EOF from both sides. 21 | - Repros fine on my ubuntu box. I am pretty sure this was tested 22 | earlier. perhaps its due to some software update in python or openssl? 23 | -------------------------------------------------------------------------------- /ca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Utility and functions to create a signed certificate for SSL. 4 | 5 | Requires M2Crypto. 6 | http://www.heikkitoivonen.net/m2crypto/ 7 | """ 8 | 9 | try : 10 | import M2Crypto 11 | except ImportError, e : 12 | print "M2Crypto is required. See http://chandlerproject.org/Projects/MeTooCrypto" 13 | raise SystemExit(1) 14 | 15 | import optparse, random, time 16 | from M2Crypto import ASN1, EVP, RSA, X509 17 | 18 | def makeRSA(n=512) : 19 | k = EVP.PKey() 20 | k.assign_rsa(RSA.gen_key(n, 65537, lambda: None)) 21 | return k 22 | 23 | def certTime(func, s) : 24 | t = ASN1.ASN1_UTCTIME() 25 | t.set_time(s) 26 | func(t) 27 | 28 | def certTimes(c, frm, to) : 29 | certTime(c.set_not_before, frm) 30 | certTime(c.set_not_after, to) 31 | 32 | def certName(**kw) : 33 | n = X509.X509_Name() 34 | for k,v in kw.items() : 35 | setattr(n, k, v) 36 | return n 37 | 38 | def makeCert(cn, ca=None, cak=None, CA=False, subjAltNames=None, bits=2048) : 39 | """ 40 | Make a certificate signed by signer (or self-signed). 41 | If CA is true, make a key for CA use, otherwise for SSL. 42 | """ 43 | k = makeRSA(bits) 44 | cert = X509.X509() 45 | chain = [cert] 46 | if cak is None : # self-signed 47 | ca,cak = cert,k 48 | else : 49 | chain.append(ca) 50 | cert.set_version(2); 51 | cert.set_serial_number(random.randint(0,0xffffffff)) # arbitrary range 52 | now = int(time.time()) 53 | certTimes(cert, now - 60*60*24*365*5, now + 60*60*24*365*5) 54 | cert.set_subject(certName(C='US', ST='CA', O='iSEC Partners', OU='port swiggers', CN=cn)) 55 | cert.set_pubkey(k) 56 | if CA : 57 | cert.add_ext(X509.new_extension('basicConstraints', 'CA:TRUE')) 58 | cert.add_ext(X509.new_extension('subjectKeyIdentifier', cert.get_fingerprint())) 59 | else : 60 | cert.add_ext(X509.new_extension('basicConstraints', 'CA:FALSE')) 61 | cert.add_ext(X509.new_extension("nsComment", "SSL Server")) # XXX? 62 | if subjAltNames != None: 63 | cert.add_ext(X509.new_extension("subjectAltName", subjAltNames)) 64 | ## XXX for CA, keyid, dirname, serial? 65 | #cert.add_ext(X509.new_extension('authorityKeyIdentifier', ca.get_fingerprint())) 66 | cert.set_issuer(ca.get_subject()) 67 | cert.sign(cak, "sha1") 68 | return chain, k 69 | 70 | pwCb = lambda *args : None 71 | 72 | def save(fn, dat) : 73 | f = file(fn, 'w') 74 | f.write(dat) 75 | f.close() 76 | 77 | def saveCerts(cs, k, name) : 78 | """Save a cert chain and its private key to a PEM file, 79 | and the first cert to a CER file.""" 80 | namePem = name + ".pem" 81 | nameCer = name + ".cer" 82 | pems = ''.join(c.as_pem() for c in cs) 83 | save(namePem, pems + k.as_pem(cipher=None)) 84 | c = cs[0] 85 | c.save(nameCer, X509.FORMAT_DER) 86 | return namePem, nameCer 87 | 88 | def loadCert(name) : 89 | """Load a cert and its private key and return them both.""" 90 | p = name + ".pem" 91 | c = X509.load_cert(p) 92 | k = EVP.PKey() 93 | k.assign_rsa(RSA.load_key(p, callback=pwCb)) 94 | return c, k 95 | 96 | def loadOrDie(name) : 97 | try : 98 | return loadCert(name) 99 | except Exception,e : 100 | fail("error loading cert %r: %s", name, e) 101 | 102 | def saveOrDie(cs, k, name) : 103 | try : 104 | return saveCerts(cs, k, name) 105 | except Exception, e : 106 | fail("error saving cert %r: %s", name, e) 107 | 108 | def getopts() : 109 | p = optparse.OptionParser(usage="usage: %prog [opts] cname") 110 | p.add_option("-C", dest="caName", default="ca", help="Name of CA file") 111 | p.add_option("-o", dest="outName", default=None, help="Name of output file") 112 | p.add_option("-c", dest="makeCA", action="store_true", help="Create a CA cert") 113 | p.add_option("-s", dest="selfSign", action="store_true", help="Create a self-signed cert") 114 | p.add_option("-a", dest="subjAltNames", default=None, help="List of subject alternative names e.g. DNS:example.com, IP:1.2.3.4, email:foo@bar.com") 115 | opt,args = p.parse_args() 116 | if opt.makeCA and len(args) == 0 : 117 | args = ["My Super CA"] 118 | if len(args) != 1 : 119 | p.error("specify a cname") 120 | if opt.outName is None : 121 | opt.outName = "ca" if opt.makeCA else "cert" 122 | opt.cname = args[0] 123 | return opt 124 | 125 | def fail(fmt, *args) : 126 | print fmt % args 127 | raise SystemExit(1) 128 | 129 | def main() : 130 | opt = getopts() 131 | if opt.selfSign or opt.makeCA : 132 | cacert, cakey = None, None 133 | else : 134 | cacert, cakey = loadOrDie(opt.caName) 135 | 136 | chain,privk = makeCert(opt.cname, CA=opt.makeCA, ca=cacert, cak=cakey, subjAltNames=opt.subjAltNames, bits=2048) 137 | 138 | names = saveOrDie(chain, privk, opt.outName) 139 | print "generated", ', '.join(names) 140 | 141 | if __name__ == '__main__' : 142 | main() 143 | 144 | -------------------------------------------------------------------------------- /docs/ca.txt: -------------------------------------------------------------------------------- 1 | 2 | NAME 3 | ca.py - a command for making SSL certificates 4 | 5 | SYNOPSIS 6 | ca.py [-o outfile] -c 7 | ca.py [-C cafile] [-o outfile] [-s] [-a altnames] cname 8 | 9 | DESCRIPTION 10 | The ca (certificate authority) tool provides a simplified interface 11 | for creating SSL certificates. It is capable of generating CA 12 | certificates and self-signed and CA-signed SSL certificates. Before 13 | creating a CA-signed certificate, a CA certificate must be generated. 14 | This is done by running ca.py with the -c option without any 15 | cname argument. A CA certificate and key will be written out to ca.pem 16 | or the file specified with the -o argument. Without the -c option, 17 | ca.py will generate an SSL certificate for cname (which is a 18 | required argument) to cert.pem or to the file specified with the 19 | -o argument. If the -s option is given, the SSL certificate will 20 | be self-signed, otherwise it will be signed with the CA key specified 21 | by the -C option or in the ca.pem file. If the -a option provides 22 | a list of altnames, they are attached to the certificate during creation. 23 | Certificate names specified with the -o and -C option should not 24 | include the ".pem" suffix. 25 | 26 | Certificates are written out to a ".pem" file and include both 27 | the certificate information as well as the RSA key. When the certificate 28 | is signed by a CA certificate, the CA certificate (but not it's private 29 | key) is also included in the file. A web server can use this file 30 | to find the private key and the entire certificate chain. For 31 | convenience the certificate is also written out in X509 DER format into 32 | a ".cer" file, but without the private key or the CA certificate. A 33 | separate script, pkcs12.sh (or pkcs12.bat) is provided to convert the 34 | ".pem" certificate into PKCS-12 format into a ".pfx" file if necessary. 35 | It requires that the openssl command line be available. 36 | 37 | 38 | EXAMPLES 39 | Generate a new CA into ca.pem and ca.pfx: 40 | 41 | $ ./ca.py -c 42 | $ ./pkcs12.sh ca 43 | 44 | Generate an SSL certificate for www.google.com into cert.pem: 45 | 46 | $ ./ca.py www.google.com 47 | 48 | Generate a self-signed certificate for www.evil.com with several 49 | alt names into foo.pem: 50 | 51 | $ ./ca.py -s -o foo \ 52 | -a "DNS:www.evil.com, IP:8.8.8.8, email:dr@evil.com" \ 53 | www.evil.com 54 | 55 | Generate an SSL certificate for www.isecpartners.com signed by 56 | AltCA.pem into isec.pem: 57 | 58 | $ ./ca.py -C AltCA -o isec www.isecpartners.com 59 | 60 | 61 | BUGS 62 | This program has no facility to specify the various fields of 63 | the certificate other than the CName. 64 | 65 | The program makes no effort to use meaningful values for the 66 | certificate serial number or version. 67 | 68 | XXX does not provide auth key identifier... 69 | 70 | -------------------------------------------------------------------------------- /docs/prox.txt: -------------------------------------------------------------------------------- 1 | 2 | NAME 3 | prox.py - A TCP proxy supporting SSL 4 | 5 | SYNOPSIS 6 | prox.py [opts] addr port 7 | prox.py [opts] -s [-C cert] [-A cname] addr port 8 | opts: [-1] [-6] [-b bindaddr] [-L localport] [-l logfile] [-m mod:args] [--ssl-in] [--ssl-out] [-3] [-T] [-O] 9 | 10 | DESCRIPTION 11 | The prox.py tool provides a TCP proxy that proxies data between 12 | a locally accepted TCP connection and a connection to a remote host. 13 | The address and port arguments are required and specify the remote 14 | host to establish a connection to. If the -6 argument is given 15 | all addresses are treated as IPv6 addresses, otherwise as IPv4 16 | addresses. When IPv6 is used, the proxy will attempt to enable 17 | IPv4-in-IPv6 compatibility if it is supported by the operating 18 | system. 19 | 20 | The prox.py tool will listen on for connections to the port port 21 | in the command line unless specified with the -L option and 22 | listen on all interfaces unless specified with the -b option. 23 | Normally the program will accept any number of TCP connections 24 | and proxy them all (possibly concurrently) unless the -1 option 25 | is given, in which case only one connection is processed. 26 | 27 | The option -O relies on SO_ORIGINAL_DST option set on the client 28 | socket by iptables REDIRECT target to get the original destination. 29 | This options does not need (and even ignores) addr and port arguments. 30 | -O (without -1) allows a single process to proxy connections to 31 | multiple destinations. As it relies on iptables, it is Linux-only. 32 | 33 | If a log filename is specified with the -l option, all data 34 | proxied by the program will be written out to the specified log 35 | file. Data is written out in lines consisting of four space-separated 36 | columns: timestamp, client address, direction and data. The 37 | timestamp is a floating point value representing seconds since 38 | UNIX epoch. The client address contains the address of the client 39 | that initiated the connection to the server in address:port 40 | format. The direction is given by the character "i" for data 41 | incoming from the client and copied to the remote host or "o" for 42 | data from the remote host and written out to the client. The 43 | data is given in hex with no spaces. A utility named proxcat.py 44 | is provided for consuming this data and presenting it to the 45 | user in a friendly manner. 46 | 47 | The prox.py tool supports SSL connections, which is specified with 48 | the --ssl-in and --ssl-out options. When --ssl-in is specified, 49 | incoming connections use the SSL protocol. When --ssl-out is 50 | specified, outgoing connections use the SSL protocol. Specifying 51 | -s is equivalent to specifying both --ssl-in and -ssl-out. If 52 | -3 is specified, the SSLv3 protocol is used. If -T is specified, 53 | TLSv1 is specified, otherwise SSLv23 is used. 54 | 55 | When using --ssl-in, an SSL certificate and private key are required. 56 | When not using the auto-cert option, the SSL certificate is read 57 | from the cert.pem file unless specified by the -C option. When using 58 | the auto-cert option, a certificate is automatically generated for 59 | the CName specified by the -A flag and is signed by the CA key in 60 | ca.pem or the key specified with the -C option. Use of the auto- 61 | certificate option requires that the M2Crypto package be installed 62 | on the system. The automatically generated certificate is written 63 | to disk as autocert.pem. 64 | 65 | All certificate file names must be specified without the ".pem" 66 | extension. More details of the certificate creation can be found in 67 | the documentation of the ca.py tool. 68 | 69 | The -m option allows multiple filter plugin modules to be specified. 70 | To specify a module, specify its name and an argument, separated 71 | by a colon. Each module is imported and initialized by calling an 72 | init function with its argument string. When proxying data, each 73 | buffer is passed to the filter function of each module, in order, 74 | before the data is forwarded and logged. The filter function should 75 | take the address, direction and buffer as an argument and returns an 76 | updated buffer. 77 | 78 | The prox.py utility can be run from a single file without any 79 | dependencies other than Python unless the -A option is used, in 80 | which case ca.py must be present and the M2Crypto package must 81 | be installed. M2Crypto is available from 82 | http://www.heikkitoivonen.net/m2crypto/ 83 | 84 | 85 | EXAMPLES 86 | 87 | Proxy data incoming to port 80 to www.google.com while logging the 88 | data to log.txt: 89 | 90 | $ ./prox.py -l log.txt www.google.com 80 91 | 92 | Proxy data incoming to port 8888 to www.google.com using SSL. 93 | The SSL connection will use cert.pem as its certificate: 94 | 95 | $ ./prox.py -s -L 8888 www.google.com 443 96 | 97 | Generate a certificate for www.evil.com signed by the key in 98 | myca.pem and use it when proxying data to the real server 99 | at 1.2.3.4 while logging data to log.txt: 100 | 101 | $ ./prox.py -s -A www.evil.com -C myca -l log.txt 1.2.3.4 443 102 | 103 | Proxy incoming data connections to port 8888 over IPv4 or IPv6 104 | to port 80 of the 173.194.64.147 IPv4 address. Notice that an 105 | IPv4-in-IPv6 address is used to specify the target. 106 | 107 | $ ./prox.py -6 -L 8888 ::ffff:173.194.64.147 80 108 | 109 | Proxy incoming data connections to port 1234 and proxy the 110 | data to 1.2.3.4 port 1234, while changing all letters back to 111 | the client to uppercase: 112 | 113 | $ ./prox.py -m modFiltCase:dummyArg 1.2.3.4 1234 114 | 115 | Proxy multiple connections on a NAT system with iptables (transparent-proxy like): 116 | $ sudo iptables -t nat -A PREROUTING -p tcp --dport 9001 -j REDIRECT --to 8888 117 | $ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to 8888 118 | $ ./prox.py -L 8888 -O 119 | 120 | 121 | BUGS 122 | The SSL handshake is performed synchronously for implementation 123 | simplicity. This could cause problems when serving multiple 124 | connections if the SSL handshake does not complete in a timely 125 | manner. 126 | 127 | The auto-certificate feature must write out the certificate to 128 | autocert.pem and then read it back in. This could lead to problems 129 | if many instances of the program are using this feature in the 130 | same directory. 131 | 132 | The server socket allows other programs to rebind over the same 133 | port. This provides convenience when repeatedly running the same 134 | proxy on the same port, but it can pose a security risk in some 135 | situations. 136 | -------------------------------------------------------------------------------- /docs/proxcat.txt: -------------------------------------------------------------------------------- 1 | 2 | NAME 3 | proxcat.py - A tool for showing the data in a prox logfile 4 | 5 | SYNOPSIS 6 | proxcat.py [-ciotxl] [-a addr] files... 7 | 8 | DESCRIPTION 9 | The proxcat.py tool processes log files created by proxcat.py 10 | and shows them to the user in various ways. It processes each 11 | of the files specified on the command line in order. In normal 12 | operation all of the data from each file is written out to 13 | standard output in the order it was received by the proxy. Data 14 | from all connections and both incoming and outgoing directions 15 | is shown interspersed. If the -i option is specified, only the 16 | data received as input from the client is shown. If the -o option 17 | is specified only the data received from the remote host and 18 | output to the client is shown. If both or neither flag is given 19 | then both input and output is shown. If an address is specified 20 | with the -a option, only data from the given connection specifier 21 | (given as an IP address and host separated by a colon) are shown. 22 | If the -t option is specified the timestamp, address and direction 23 | of data is printed out before each section of data. If the -c option 24 | is specified all data in the out direction is colored red and 25 | all data in the output direction is colored green. If the 26 | -x flag is specified the data is shown in hexdump format rather 27 | than literally. Finally, if the -l flag is given, a list of each 28 | session is shown by printing the timestamp and address for the first 29 | occurance of each address in the log. 30 | 31 | 32 | EXAMPLES 33 | 34 | List all sessions in log.txt: 35 | 36 | $ ./proxcat.py -l log.txt 37 | 38 | Dump out the data in all log*.txt files: 39 | 40 | $ ./proxcat.py log*.txt 41 | 42 | Dump out the data in log.txt from the client at 127.0.0.1:3333 as 43 | hex with color and timestamps: 44 | 45 | $ ./prox.py -xtc -a 127.0.0.1:3333 log.txt 46 | 47 | Dump out only the data sent from 127.0.0.1:3333 to the proxy: 48 | 49 | $ ./prox.py -i -a 127.0.0.1:3333 log.txt 50 | 51 | 52 | BUGS 53 | Color codes are sent inline in the standard output stream. If 54 | the output stream is truncated (for example by piping the data to 55 | "head") the color-reset code might not be received and the terminal 56 | will be left in an unusual color mode. 57 | 58 | 59 | -------------------------------------------------------------------------------- /modFiltCase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | A small example filter module. 4 | This module will change all outgoing data to uppercase. 5 | """ 6 | 7 | def init(argstr) : 8 | print "modFiltCase initialized with %r" % argstr 9 | 10 | def filter(addr, dir, buf) : 11 | if dir == 'o' : 12 | buf = buf.upper() 13 | return buf 14 | 15 | -------------------------------------------------------------------------------- /pkcs12.bat: -------------------------------------------------------------------------------- 1 | @rem Convert a PEM certificate to PKCS12 using OpenSSL 2 | 3 | @rem openssl pkcs12 -export -out %1.pfx -inkey %1.pem -in %1.pem -certfile ca.pem 4 | openssl pkcs12 -export -out %1.pfx -inkey %1.pem -in %1.pem 5 | -------------------------------------------------------------------------------- /pkcs12.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Convert a PEM certificate to PKCS12 using OpenSSL 4 | # 5 | 6 | if [ $# -ne 1 ] ; then 7 | echo "usage: $0 name" 8 | exit 1 9 | fi 10 | 11 | #openssl pkcs12 -export -out $1.pfx -inkey $1.pem -in $1.pem -certfile ca.pem 12 | openssl pkcs12 -export -out $1.pfx -inkey $1.pem -in $1.pem 13 | -------------------------------------------------------------------------------- /prox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | TCP Proxy server. Listens on a port for connections, initiates 4 | a connection to the real server, and copies data between the 5 | two connections. Optionally logs the data. 6 | 7 | TODO: 8 | - non-blocking connect ? 9 | - possibly do non-blocking ssl handshaking? 10 | - cleaner shutdown? 11 | """ 12 | 13 | from socket import * 14 | import errno, optparse, os, platform, socket, ssl, struct, time 15 | from select import * 16 | 17 | class Error(Exception) : 18 | pass 19 | 20 | def fail(fmt, *args) : 21 | print "error:", fmt % args 22 | raise SystemExit(1) 23 | 24 | def tcpListen(six, addr, port, blk, sslProto, cert=None, key=None) : 25 | """Return a listening server socket.""" 26 | s = socket.socket(AF_INET6 if six else AF_INET, SOCK_STREAM) 27 | if six and hasattr(socket, 'IPV6_V6ONLY') : 28 | s.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) 29 | s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 30 | s.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) 31 | if sslProto is not None : 32 | if not os.path.exists(cert) : 33 | fail("cert file %s doesnt exist", cert) 34 | if key and not os.path.exists(key) : 35 | fail("cert key %s doesnt exist", key) 36 | s = ssl.wrap_socket(s, ssl_version=sslProto, server_side=True, certfile=cert, keyfile=key) 37 | s.bind((addr,port)) 38 | s.listen(5) 39 | s.setblocking(blk) 40 | return s 41 | 42 | def tcpConnect(six, addr, port, blk, sslProto, clientCert=None) : 43 | """Returned a connected client socket (blocking on connect...)""" 44 | s = socket.socket(AF_INET6 if six else AF_INET, SOCK_STREAM) 45 | s.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) 46 | if sslProto is not None : 47 | if clientCert is None: 48 | certfile = None 49 | keyfile = None 50 | else: 51 | certfile = clientCert + '.cert' 52 | keyfile = clientCert + '.key' 53 | s = ssl.wrap_socket(s, certfile=certfile, keyfile=keyfile, cert_reqs=ssl.CERT_NONE, ssl_version=sslProto) 54 | s.connect((addr,port)) 55 | s.setblocking(blk) 56 | return s 57 | 58 | def safeClose(x) : 59 | try : 60 | x.close() 61 | except Exception, e : 62 | pass 63 | 64 | def getSslVers(opt, enable) : 65 | if enable : 66 | if opt.sslV3 : 67 | return ssl.PROTOCOL_SSLv3 68 | elif opt.TLS : 69 | return ssl.PROTOCOL_TLSv1 70 | else : 71 | return ssl.PROTOCOL_SSLv23 72 | 73 | class Server(object) : 74 | def __init__(self, opt, q) : 75 | self.opt = opt 76 | sslCert = opt.cert + ".pem" 77 | ver = getSslVers(opt, opt.sslIn) 78 | self.sock = tcpListen(opt.ip6, opt.bindAddr, opt.locPort, 0, ver, sslCert, None) 79 | self.q = q 80 | def preWait(self, rr, r, w, e) : 81 | r.append(self.sock) 82 | def postWait(self, r, w, e) : 83 | if self.sock in r : 84 | try : 85 | cl,addr = self.sock.accept() 86 | except ssl.SSLError, e : 87 | print "ssl error during accept", e 88 | return 89 | cl.setblocking(0) 90 | self.q.append(Proxy(self.opt, cl, addr)) 91 | if self.opt.oneshot : 92 | safeClose(self.sock) 93 | return 'elvis has left the building' 94 | 95 | class Half(object) : 96 | """a single connection""" 97 | def __init__(self, opt, sock, addr, dir) : 98 | self.opt = opt 99 | self.sock = sock 100 | self.addr = addr 101 | self.dir = dir 102 | 103 | self.name = "peer" if self.dir else "client" 104 | self.queue = [] 105 | self.dest = None 106 | self.err = None 107 | self.ready = False 108 | 109 | # XXX handle ssl 110 | 111 | def preWait(self, rr, r, w, e) : 112 | if self.ready : 113 | rr.append(self.sock) 114 | r.append(self.sock) 115 | if self.queue : 116 | w.append(self.sock) 117 | def postWait(self, r, w, e) : 118 | if not self.err and self.sock in w and self.queue : 119 | self.writeSome() 120 | if not self.err and self.sock in r : 121 | self.ready = True 122 | self.copy() 123 | return self.err 124 | 125 | def error(self, msg, e) : 126 | print "%s on %s: %r %s" % (msg, self.name, e, e) 127 | self.err = "error on " + self.name 128 | return self.err 129 | 130 | def writeSome(self) : 131 | try : 132 | n = self.sock.send(self.queue[0]) 133 | except ssl.SSLError, e : 134 | # XXX can we get WantRead here? 135 | if e.args[0] == ssl.SSL_ERROR_WANT_WRITE : 136 | n = 0 137 | else : 138 | return self.error("send ssl error", e) 139 | except Exception, e : 140 | return self.error("send error", e) 141 | if n != len(self.queue[0]) : 142 | self.queue[0] = self.queue[0][n:] 143 | else : 144 | del self.queue[0] 145 | 146 | def copy(self) : 147 | try : 148 | buf = self.sock.recv(4096) 149 | except ssl.SSLError, e : 150 | # XXX can we get WantWrite here? 151 | if e.args[0] == ssl.SSL_ERROR_WANT_READ : 152 | self.ready = False 153 | return 154 | if e.args[0] == ssl.SSL_ERROR_EOF : 155 | return self.error("eof", e) 156 | return self.error("recv ssl error", e) 157 | except socket.error, e : 158 | if e.errno == errno.EWOULDBLOCK : 159 | self.ready = False 160 | return 161 | return self.error("recv socket error", e) 162 | except Exception, e : 163 | return self.error("recv error", e) 164 | if len(buf) == 0 : 165 | return self.error("eof", 0) 166 | for mod in self.opt.filters : 167 | buf = mod.filter(self.addr, self.dir, buf) 168 | self.dest.queue.append(buf) 169 | if self.opt.log : 170 | now = time.time() 171 | a = '%s:%s' % self.addr 172 | self.opt.log.write("%f %s %s %s\n" % (now, a, self.dir, buf.encode('hex'))) 173 | self.opt.log.flush() 174 | def close(self) : 175 | safeClose(self.sock) 176 | 177 | class Proxy(object) : 178 | """A client connection and the peer connection he proxies to""" 179 | def __init__(self, opt, sock, addr) : 180 | print "New client %s" % (addr,) 181 | self.opt = opt 182 | 183 | if opt.originalDst : 184 | try : 185 | sockaddr_in = sock.getsockopt(socket.SOL_IP, 80, 16) # SO_ORIGINAL_DST = 80 186 | except socket.error : 187 | raise Error("SO_ORIGINAL_DST not supported on this platform") 188 | _, port, a, b, c, d = struct.unpack('!HHBBBB', sockaddr_in[:8]) 189 | print('Original destination was: %d.%d.%d.%d:%d' % (a, b, c, d, port)) 190 | self.opt.addr = '%d.%d.%d.%d' % (a, b, c, d) 191 | self.opt.port = port 192 | 193 | self.cl = Half(opt, sock, addr, 'i') 194 | # note: blocking connect for simplicity for now... 195 | ver = getSslVers(opt, opt.sslOut) 196 | peer = tcpConnect(opt.ip6, opt.addr, opt.port, 0, ver, opt.clientCert) 197 | self.peer = Half(opt, peer, addr, 'o') 198 | 199 | self.cl.dest = self.peer 200 | self.peer.dest = self.cl 201 | self.err = None 202 | 203 | def preWait(self, rr, r, w, e) : 204 | self.cl.preWait(rr, r,w,e) 205 | self.peer.preWait(rr, r,w,e) 206 | def postWait(self, r, w, e) : 207 | if not self.err : 208 | self.err = self.cl.postWait(r,w,e) 209 | if not self.err : 210 | self.err = self.peer.postWait(r,w,e) 211 | if self.err : 212 | self.cl.close() 213 | self.peer.close() 214 | return self.err 215 | 216 | def serverLoop(opt) : 217 | qs = [] 218 | qs.append(Server(opt, qs)) 219 | while qs : 220 | # note: rr holds "read already ready" 221 | # meaning it wasnt fully drained last time 222 | rr,r,w,e = [], [], [], [] 223 | for q in qs : 224 | q.preWait(rr, r, w, e) 225 | timeo = 10.0 if not rr else 0.0 226 | r,w,e = select(r, w, e, timeo) 227 | r = set(r).union(rr) 228 | for q in qs : 229 | if q.postWait(r, w, e) : 230 | qs.remove(q) 231 | print 'done' 232 | 233 | def autoCert(cn, caName, name) : 234 | """Create a certificate signed by caName for cn into name.""" 235 | import ca # requires M2Crypto! 236 | cac, cak = ca.loadOrDie(caName) 237 | c,k = ca.makeCert(cn, ca=cac, cak=cak) 238 | ca.saveOrDie(c, k, name) 239 | 240 | def getopts() : 241 | p = optparse.OptionParser(usage="usage: %prog [opts] addr port") 242 | p.add_option('-6', dest='ip6', action='store_true', help="Use IPv6") 243 | p.add_option("-b", dest="bindAddr", default="0.0.0.0", help="Address to bind to") 244 | p.add_option("-L", dest="locPort", type="int", help="Local port to listen on") 245 | p.add_option("-s", dest="ssl", action="store_true", help="Use SSL for incoming and outgoing connections") 246 | p.add_option("--ssl-in", dest="sslIn", action="store_true", help="Use SSL for incoming connections") 247 | p.add_option("--ssl-out", dest="sslOut", action="store_true", help="Use SSL for outgoing connections") 248 | p.add_option('-3', dest='sslV3', action='store_true', help='Use SSLv3 protocol') 249 | p.add_option('-T', dest='TLS', action='store_true', help='Use TLSv1 protocol') 250 | p.add_option("-C", dest="cert", default=None, help="Cert for SSL") 251 | p.add_option("-A", dest="autoCname", action="store", help="CName for Auto-generated SSL cert") 252 | p.add_option('-1', dest='oneshot', action='store_true', help="Handle a single connection") 253 | p.add_option("-l", dest="logFile", help="Filename to log to") 254 | p.add_option("-m", dest="modules", action="append", default=[], help="filtering modules") 255 | p.add_option("-O", dest="originalDst", action="store_true", help="Use SO_ORIGINAL_DST for destination") 256 | p.add_option("-c", dest="clientCert", default=None, help="Client certificate to present to server") 257 | opt,args = p.parse_args() 258 | if opt.ssl : 259 | opt.sslIn = True 260 | opt.sslOut = True 261 | if opt.sslV3 and opt.TLS : 262 | p.error("-3 and -T cannot be used together") 263 | if opt.bindAddr == '0.0.0.0' and opt.ip6 : 264 | opt.bindAddr = '::' 265 | if opt.originalDst and platform.system() != "Linux" : 266 | p.error("SO_ORIGINAL_DST is only supported in Linux systems") 267 | if len(args) != 2 and not opt.originalDst : 268 | p.error("specify address and port or use -O") 269 | if opt.cert is None : 270 | opt.cert = "ca" if opt.autoCname else "cert" 271 | if opt.ssl and opt.cert is None : 272 | if opt.autoCname is not None : 273 | p.error("specify CA cert") 274 | else : 275 | p.error("specify SSL cert") 276 | if not opt.originalDst : 277 | opt.addr = args[0] 278 | try : 279 | opt.port = int(args[1]) 280 | except ValueError : 281 | p.error("invalid port: " + args[1]) 282 | if opt.locPort == None : 283 | if opt.originalDst : 284 | p.error("-O requires -L") 285 | else : 286 | opt.locPort = opt.port 287 | return opt 288 | 289 | def initModule(modstr) : 290 | modname, modargs = modstr.split(':', 1) if ':' in modstr else (modstr,"") 291 | try : 292 | mod = __import__(modname) 293 | except ImportError : 294 | fail("could not import %r" % modname) 295 | mod.init(modargs) 296 | return mod 297 | 298 | def main() : 299 | opt = getopts() 300 | if opt.sslIn and opt.autoCname : 301 | autoCert(opt.autoCname, opt.cert, "autocert") 302 | opt.cert = "autocert" 303 | opt.log = file(opt.logFile, 'w') if opt.logFile else None 304 | opt.filters = map(initModule, opt.modules) 305 | serverLoop(opt) 306 | 307 | if __name__ == '__main__' : 308 | main() 309 | 310 | -------------------------------------------------------------------------------- /proxcat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Dumps the output from a proxy log. 4 | 5 | Todo: 6 | - timed replay? 7 | """ 8 | 9 | import optparse, sys, time 10 | 11 | def tfmt(t) : 12 | return time.strftime('%Y-%M-%d_%H:%M:%S', time.localtime(t)) 13 | 14 | reset = "\x1b[m" 15 | red = "\x1b[31m" 16 | green = "\x1b[32m" 17 | 18 | def hex(off, dat) : 19 | def datAt(n) : 20 | if n >= 0 and n < len(dat) : 21 | return dat[n] 22 | def hexIt(ch) : 23 | if ch is None : 24 | return ' ' 25 | return '%02x' % ord(ch) 26 | def ascIt(ch) : 27 | if ch is None : 28 | return ' ' 29 | if ch >= ' ' and ch <= '~' : 30 | return ch 31 | return '.' 32 | 33 | quant = 16 # power of 2 34 | d = off & (quant - 1) 35 | off,n = off-d, 0-d 36 | ls = [] 37 | while n < len(dat) : 38 | hex1 = ' '.join(hexIt(datAt(n+m)) for m in xrange(0, 8)) 39 | hex2 = ' '.join(hexIt(datAt(n+m)) for m in xrange(8, 16)) 40 | asc = ''.join(ascIt(datAt(n+m)) for m in xrange(0, 16)) 41 | l = '%08x: %-23s : %-23s | %s' % (off, hex1, hex2, asc) 42 | sys.stdout.write(l) 43 | sys.stdout.write('\n') 44 | off,n = off+quant, n+quant 45 | 46 | def parsedLines(fn) : 47 | for l in file(fn, 'r') : 48 | ts,addr,dir,dat = l.strip().split(' ') 49 | ts = float(ts) 50 | yield ts,addr,dir,dat 51 | 52 | def cat(opt, fn, seen) : 53 | offs = dict() 54 | for ts,addr,dir,dat in parsedLines(fn) : 55 | if opt.addr is not None and addr != opt.addr : 56 | continue 57 | if opt.list : 58 | if addr not in seen : 59 | sys.stdout.write("%s %s\n" % (tfmt(ts), addr)) 60 | seen.add(addr) 61 | continue 62 | dat = dat.decode('hex') 63 | if dir == 'o' and opt.output or dir == 'i' and opt.input : 64 | if opt.timestamp : 65 | sys.stdout.write("\n%s %s %s: " % (tfmt(ts), addr, dir)) 66 | if opt.hex : 67 | sys.stdout.write('\n') 68 | if opt.color : 69 | sys.stdout.write(green if dir == 'o' else red) 70 | if opt.hex : 71 | k = addr,dir 72 | off = offs.get(k, 0) 73 | offs[k] = off + len(dat) 74 | hex(off, dat) 75 | else : 76 | sys.stdout.write(dat) 77 | if opt.color : 78 | sys.stdout.write(reset) 79 | sys.stdout.flush() 80 | 81 | def getopts() : 82 | p = optparse.OptionParser(usage="usage: %prog [opts] files...") 83 | p.add_option("-i", dest="input", action="store_true", help="show data from client") 84 | p.add_option("-o", dest="output", action="store_true", help="show data to client") 85 | p.add_option("-t", dest="timestamp", action="store_true", help="show timestamp and other metadata") 86 | p.add_option("-x", dest="hex", action="store_true", help="show data in hex") 87 | p.add_option('-a', dest="addr", help="only show data from addr") 88 | p.add_option('-c', dest='color', action="store_true", help="use colors") 89 | p.add_option('-l', dest='list', action='store_true', help='list sessions') 90 | opt,args = p.parse_args() 91 | opt.files = args 92 | 93 | if not opt.input and not opt.output : 94 | opt.input = True 95 | opt.output = True 96 | return opt 97 | 98 | def main() : 99 | opt = getopts() 100 | seen = set() 101 | for fn in opt.files : 102 | cat(opt, fn, seen) 103 | 104 | if __name__ == '__main__' : 105 | main() 106 | 107 | --------------------------------------------------------------------------------