├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── debian ├── changelog ├── compat ├── control ├── copyright ├── docs ├── etc │ ├── default │ │ └── qtunnel │ └── init.d │ │ └── qtunnel ├── files ├── install ├── postinst ├── prerm └── rules └── src ├── qtunnel └── main.go └── tunnel ├── cipher.go ├── cipher_test.go ├── conn.go ├── conn_test.go ├── recycler.go ├── tunnel.go └── tunnel_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | bin/ 3 | debian/qtunnel* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 getqujing.com 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPATH := $(shell pwd) 2 | .PHONY: clean test 3 | 4 | all: 5 | @GOPATH=$(GOPATH) go install qtunnel 6 | 7 | clean: 8 | @rm -fr bin pkg 9 | 10 | test: 11 | @GOPATH=$(GOPATH) go test tunnel 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qTunnel 2 | 3 | ### qTunnel - a simpler and (possibily) faster tunnel program 4 | 5 | `qtunnel` is a network tunneling software working as an encryption wrapper between clients and servers (remote/local). It can work as a Stunnel/stud replacement. 6 | 7 | `qtunnel` has been serving over 10 millions connections on [Qu Jing](http://getqujing.com) each day for the past few months. 8 | 9 | ##### Why Another Wrapper 10 | 11 | [Stunnel](https://www.stunnel.org/index.html)/[stud](https://github.com/bumptech/stud) is great in SSL/TLS based environments, but what we want is a lighter and faster solution that only does one job: transfer encrypted data between servers and clients. We don't need to deal with certification settings and we want the transfer is as fast as possible. So we made qTunnel. Basically, it's a Stunnel/stud without certification settings and SSL handshakes, and it's written in Go. 12 | 13 | ### Requirements 14 | 15 | qtunnel is writen in [golang 1.3.1](http://golang.org/dl/), after building it can run on almost every OS. 16 | 17 | ### Build 18 | 19 | To build `qtunnel` 20 | 21 | `$ make` 22 | 23 | To test `qtunnel` 24 | 25 | `$ make test` 26 | 27 | ### Usage 28 | 29 | $ ./bin/qtunnel -h 30 | Usage of ./bin/qtunnel: 31 | -backend="127.0.0.1:6400": host:port of the backend 32 | -clientmode=false: if running at client mode 33 | -crypto="rc4": encryption method 34 | -listen=":9001": host:port qtunnel listen on 35 | -logto="stdout": stdout or syslog 36 | -secret="secret": password used to encrypt the data 37 | 38 | `qtunnel` supports two encryption methods: `rc4` and `aes256cfb`. Both servers and clients should use the same `crypto` and same `secret`. 39 | 40 | ### Example 41 | 42 | Let's say, you have a `redis` server on `host-a`, you want to connect to it from `host-b`, normally, just use: 43 | 44 | $ redis-cli -h host-a -p 6379 45 | 46 | will do the job. The topology is: 47 | 48 | redis-cli (host-b) <------> (host-a) redis-server 49 | 50 | If the host-b is in some insecure network environment, i.e. another data center or another region, the clear-text based redis porocol is not good enough, you can use `qtunnel` as a secure wrapper 51 | 52 | On `host-b`: 53 | 54 | $ qtunnel -listen=127.1:6379 -backend=host-a:6378 -clientmode=true -secret=secret -crypto=rc4 55 | 56 | On `host-a`: 57 | 58 | $ qtunnel -listen=:6378 -backend=127.1:6379 -secret=secret -crypto=rc4 59 | 60 | Then connect on `host-b` as: 61 | 62 | $ redis-cli -h 127.1 -p 6379 63 | 64 | This will establish a secure tunnel between your `redis-cli` and `redis` server, the topology is: 65 | 66 | redis-cli (host-b) <--> qtunnel (client,host-b) <--> qtunnel (host-a) <--> redis-server 67 | 68 | After this, you can communicate over a encrypted wrapper rather than clear text. 69 | 70 | ### Credits 71 | 72 | Special thanks to [Paul](http://paulrosenzweig.com) for reviewing the code. 73 | 74 | ### Contributing 75 | 76 | We encourage you to contribute to `qtunnel`! Please feel free to [submit a bug report](https://github.com/getqujing/qtunnel/issues), [fork the repo](https://github.com/getqujing/qtunnel/fork) or [create a pull request](https://github.com/getqujing/qtunnel/pulls). 77 | 78 | ### License 79 | 80 | `qtunnel` is released under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 81 | 82 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | qtunnel (0.1.0-1) unstable; urgency=low 2 | 3 | * Initial release 4 | 5 | -- Zhou Rui Sat, 02 Aug 2014 12:33:02 +0800 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: qtunnel 2 | Section: net 3 | Priority: optional 4 | Maintainer: Zhou Rui 5 | Build-Depends: debhelper (>= 7.0.50~) 6 | Standards-Version: 3.8.4 7 | Homepage: https://github.com/getqujing/qtunnel 8 | Vcs-Git: git://github.com/getqujing/qtunnel.git 9 | #Vcs-Browser: http://git.debian.org/?p=collab-maint/qtunnel.git;a=summary 10 | 11 | Package: qtunnel 12 | Architecture: any 13 | Depends: ${shlibs:Depends} 14 | Description: secure transport tunnel 15 | Secure transport tunnel from getqujing.com. 16 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getqujing/qtunnel/ef8680dfe0ff23ebf44dc78039adf4e4ebf31a26/debian/docs -------------------------------------------------------------------------------- /debian/etc/default/qtunnel: -------------------------------------------------------------------------------- 1 | CLIENT_MODE=false 2 | LISTEN=":6400" 3 | BACKEND="127.0.0.1:3128" 4 | SECRET=secret 5 | LOGTO=syslog 6 | -------------------------------------------------------------------------------- /debian/etc/init.d/qtunnel: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: qtunnel 4 | # Required-Start: $network $local_fs 5 | # Required-Stop: 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: 9 | # Description: 10 | # <...> 11 | # <...> 12 | ### END INIT INFO 13 | 14 | # Author: Zhou Rui 15 | 16 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 17 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 18 | DESC=qtunnel 19 | NAME=qtunnel 20 | DAEMON=/usr/bin/qtunnel 21 | PIDFILE=/var/run/$NAME.pid 22 | SCRIPTNAME=/etc/init.d/$NAME 23 | 24 | # Exit if the package is not installed 25 | [ -x $DAEMON ] || exit 0 26 | 27 | # Read configuration variable file if it is present 28 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 29 | DAEMON_ARGS="-backend=$BACKEND -clientmode=$CLIENT_MODE -listen=$LISTEN -secret=$SECRET -logto=$LOGTO" 30 | 31 | # Load the VERBOSE setting and other rcS variables 32 | . /lib/init/vars.sh 33 | 34 | # Define LSB log_* functions. 35 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. 36 | . /lib/lsb/init-functions 37 | 38 | # 39 | # Function that starts the daemon/service 40 | # 41 | do_start() 42 | { 43 | # Return 44 | # 0 if daemon has been started 45 | # 1 if daemon was already running 46 | # 2 if daemon could not be started 47 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ 48 | || return 1 49 | start-stop-daemon --start --quiet --background --pidfile $PIDFILE --exec $DAEMON -- \ 50 | $DAEMON_ARGS \ 51 | || return 2 52 | # Add code here, if necessary, that waits for the process to be ready 53 | # to handle requests from services started subsequently which depend 54 | # on this one. As a last resort, sleep for some time. 55 | } 56 | 57 | # 58 | # Function that stops the daemon/service 59 | # 60 | do_stop() 61 | { 62 | # Return 63 | # 0 if daemon has been stopped 64 | # 1 if daemon was already stopped 65 | # 2 if daemon could not be stopped 66 | # other if a failure occurred 67 | # Wait for children to finish too if this is a daemon that forks 68 | # and if the daemon is only ever run from this initscript. 69 | # If the above conditions are not satisfied then add some other code 70 | # that waits for the process to drop all resources that could be 71 | # needed by services started subsequently. A last resort is to 72 | # sleep for some time. 73 | start-stop-daemon --stop --quiet --exec $DAEMON 74 | [ "$?" = 2 ] && return 2 75 | rm -f $PIDFILE 76 | sleep 1 77 | return "0" 78 | } 79 | 80 | # 81 | # Function that sends a SIGHUP to the daemon/service 82 | # 83 | do_reload() { 84 | # 85 | # If the daemon can reload its configuration without 86 | # restarting (for example, when it is sent a SIGHUP), 87 | # then implement that here. 88 | # 89 | start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME 90 | return 0 91 | } 92 | 93 | case "$1" in 94 | start) 95 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" 96 | do_start 97 | case "$?" in 98 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 99 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 100 | esac 101 | ;; 102 | stop) 103 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 104 | do_stop 105 | case "$?" in 106 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 107 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 108 | esac 109 | ;; 110 | status) 111 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 112 | ;; 113 | #reload|force-reload) 114 | # 115 | # If do_reload() is not implemented then leave this commented out 116 | # and leave 'force-reload' as an alias for 'restart'. 117 | # 118 | #log_daemon_msg "Reloading $DESC" "$NAME" 119 | #do_reload 120 | #log_end_msg $? 121 | #;; 122 | restart|force-reload) 123 | # 124 | # If the "reload" option is implemented then remove the 125 | # 'force-reload' alias 126 | # 127 | log_daemon_msg "Restarting $DESC" "$NAME" 128 | do_stop 129 | case "$?" in 130 | 0|1) 131 | do_start 132 | case "$?" in 133 | 0) log_end_msg 0 ;; 134 | 1) log_end_msg 1 ;; # Old process is still running 135 | *) log_end_msg 1 ;; # Failed to start 136 | esac 137 | ;; 138 | *) 139 | # Failed to stop 140 | log_end_msg 1 141 | ;; 142 | esac 143 | ;; 144 | *) 145 | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 146 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 147 | exit 3 148 | ;; 149 | esac 150 | 151 | : 152 | -------------------------------------------------------------------------------- /debian/files: -------------------------------------------------------------------------------- 1 | qtunnel_0.1.0-1_amd64.deb net optional 2 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | bin/qtunnel /usr/bin 2 | debian/etc/init.d/qtunnel /etc/init.d 3 | debian/etc/default/qtunnel /etc/default 4 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for qtunnel 3 | 4 | set -e 5 | 6 | update-rc.d qtunnel defaults 90 >/dev/null 7 | 8 | invoke-rc.d qtunnel start 9 | 10 | exit 0 11 | -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # prerm script for qtunnel 3 | 4 | set -e 5 | 6 | invoke-rc.d qtunnel stop 7 | 8 | exit 0 9 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | -------------------------------------------------------------------------------- /src/qtunnel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | "log" 8 | "log/syslog" 9 | "flag" 10 | "tunnel" 11 | ) 12 | 13 | func waitSignal() { 14 | var sigChan = make(chan os.Signal, 1) 15 | signal.Notify(sigChan) 16 | for sig := range sigChan { 17 | if sig == syscall.SIGINT || sig == syscall.SIGTERM { 18 | log.Printf("terminated by signal %v\n", sig) 19 | return 20 | } else { 21 | log.Printf("received signal: %v, ignore\n", sig) 22 | } 23 | } 24 | } 25 | 26 | func main() { 27 | var faddr, baddr, cryptoMethod, secret, logTo string 28 | var clientMode bool 29 | flag.StringVar(&logTo, "logto", "stdout", "stdout or syslog") 30 | flag.StringVar(&faddr, "listen", ":9001", "host:port qtunnel listen on") 31 | flag.StringVar(&baddr, "backend", "127.0.0.1:6400", "host:port of the backend") 32 | flag.StringVar(&cryptoMethod, "crypto", "rc4", "encryption method") 33 | flag.StringVar(&secret, "secret", "secret", "password used to encrypt the data") 34 | flag.BoolVar(&clientMode, "clientmode", false, "if running at client mode") 35 | flag.Parse() 36 | 37 | log.SetOutput(os.Stdout) 38 | if logTo == "syslog" { 39 | w, err := syslog.New(syslog.LOG_INFO, "qtunnel") 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | log.SetOutput(w) 44 | } 45 | 46 | t := tunnel.NewTunnel(faddr, baddr, clientMode, cryptoMethod, secret, 4096) 47 | log.Println("qtunnel started.") 48 | go t.Start() 49 | waitSignal() 50 | } 51 | -------------------------------------------------------------------------------- /src/tunnel/cipher.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "log" 5 | "crypto/rc4" 6 | "crypto/md5" 7 | "crypto/aes" 8 | "crypto/cipher" 9 | ) 10 | 11 | type Cipher struct { 12 | enc cipher.Stream 13 | dec cipher.Stream 14 | } 15 | 16 | type chiperCreator func(key []byte) (*Cipher, error) 17 | 18 | var cipherMap = map[string]chiperCreator { 19 | "rc4": newRC4Cipher, 20 | "aes256cfb": newAES256CFBCipher, 21 | } 22 | 23 | func secretToKey(secret []byte, size int) []byte { 24 | // size mod 16 must be 0 25 | h := md5.New() 26 | buf := make([]byte, size) 27 | count := size / md5.Size 28 | // repeatly fill the key with the secret 29 | for i := 0; i < count; i++ { 30 | h.Write(secret) 31 | copy(buf[md5.Size*i:md5.Size*(i+1)-1], h.Sum(nil)) 32 | } 33 | return buf 34 | } 35 | 36 | func newRC4Cipher(secret []byte) (*Cipher, error) { 37 | ec, err := rc4.NewCipher(secretToKey(secret, 16)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | dc := *ec 42 | 43 | return &Cipher{ec, &dc}, nil 44 | } 45 | 46 | func newAES256CFBCipher(secret []byte) (*Cipher, error) { 47 | key := secretToKey(secret, 32) 48 | block, err := aes.NewCipher(key) 49 | if err != nil { 50 | return nil, err 51 | } 52 | ec := cipher.NewCFBEncrypter(block, key[:block.BlockSize()]) 53 | dc := cipher.NewCFBDecrypter(block, key[:block.BlockSize()]) 54 | 55 | return &Cipher{ec, dc}, nil 56 | } 57 | 58 | func NewCipher(cryptoMethod string, secret []byte) *Cipher { 59 | cc := cipherMap[cryptoMethod] 60 | if cc == nil { 61 | log.Fatalf("unsupported crypto method %s", cryptoMethod) 62 | } 63 | c, err := cc(secret) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | return c 68 | } 69 | 70 | func (c *Cipher) encrypt(dst, src []byte) { 71 | c.enc.XORKeyStream(dst, src) 72 | } 73 | 74 | func (c *Cipher) decrypt(dst, src []byte) { 75 | c.dec.XORKeyStream(dst, src) 76 | } 77 | -------------------------------------------------------------------------------- /src/tunnel/cipher_test.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRC4(t *testing.T) { 8 | secret := []byte("testsecret") 9 | clearText := "thisISaCLEARtext" 10 | c := NewCipher("rc4", secret) 11 | dst := make([]byte, len(clearText)) 12 | dst2 := make([]byte, len(clearText)) 13 | c.encrypt(dst, []byte(clearText)) 14 | c.decrypt(dst2, dst) 15 | if (clearText != string(dst2)) { 16 | t.Error(string(dst2)) 17 | } 18 | } 19 | 20 | func TestAES256CFB(t *testing.T) { 21 | secret := []byte("testsecret") 22 | clearText := "thisISaCLEARtext" 23 | c := NewCipher("aes256cfb", secret) 24 | dst := make([]byte, len(clearText)) 25 | dst2 := make([]byte, len(clearText)) 26 | c.encrypt(dst, []byte(clearText)) 27 | c.decrypt(dst2, dst) 28 | if (clearText != string(dst2)) { 29 | t.Error(string(dst2)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/tunnel/conn.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type Conn struct { 9 | conn net.Conn 10 | cipher *Cipher 11 | pool *recycler 12 | } 13 | 14 | func NewConn(conn net.Conn, cipher *Cipher, pool *recycler) *Conn { 15 | return &Conn{ 16 | conn: conn, 17 | cipher: cipher, 18 | pool: pool, 19 | } 20 | } 21 | 22 | func (c *Conn) Read(b []byte) (int, error) { 23 | c.conn.SetReadDeadline(time.Now().Add(30 * time.Minute)) 24 | if c.cipher == nil { 25 | return c.conn.Read(b) 26 | } 27 | n, err := c.conn.Read(b) 28 | if n > 0 { 29 | c.cipher.decrypt(b[0:n], b[0:n]) 30 | } 31 | return n, err 32 | } 33 | 34 | func (c *Conn) Write(b []byte) (int, error) { 35 | if c.cipher == nil { 36 | return c.conn.Write(b) 37 | } 38 | c.cipher.encrypt(b, b) 39 | return c.conn.Write(b) 40 | } 41 | 42 | func (c *Conn) Close() { 43 | c.conn.Close() 44 | } 45 | 46 | func (c *Conn) CloseRead() { 47 | if conn, ok := c.conn.(*net.TCPConn); ok { 48 | conn.CloseRead() 49 | } 50 | } 51 | 52 | func (c *Conn) CloseWrite() { 53 | if conn, ok := c.conn.(*net.TCPConn); ok { 54 | conn.CloseWrite() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/tunnel/conn_test.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "testing" 5 | "net" 6 | "bytes" 7 | "math/rand" 8 | ) 9 | 10 | func makeTestBuf() ([]byte, []byte) { 11 | buf1 := make([]byte, rand.Intn(2048000) + 20480000) 12 | for i := 0; i <= 100; i++ { 13 | buf1[rand.Intn(len(buf1))] = 1 14 | } 15 | buf2 := make([]byte, len(buf1)) 16 | copy(buf1, buf2) 17 | return buf1, buf2 18 | } 19 | 20 | func TestWrite(t *testing.T) { 21 | done := make(chan bool) 22 | secret := []byte("secret") 23 | data, data2 := makeTestBuf() 24 | pool := NewRecycler(4) 25 | ln, err := net.Listen("tcp", "127.0.0.1:9444") 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | defer ln.Close() 30 | go func() { 31 | conn, err := ln.Accept() 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | defer conn.Close() 36 | cipher := NewCipher("rc4", secret) 37 | buf := make([]byte, len(data)) 38 | total := 0 39 | n, err := conn.Read(buf) 40 | for { 41 | total += n 42 | if total < len(data) { 43 | n, err = conn.Read(buf[total:len(buf)]) 44 | if err != nil { 45 | break 46 | } 47 | } else { 48 | break 49 | } 50 | } 51 | cipherData := make([]byte, len(data)) 52 | cipher.encrypt(cipherData, data) 53 | if (bytes.Compare(cipherData, buf) != 0) { 54 | t.Fail() 55 | } 56 | close(done) 57 | }() 58 | conn, err := net.Dial("tcp", "127.0.0.1:9444") 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | conn2 := NewConn(conn, NewCipher("rc4", secret), pool) 63 | defer conn2.Close() 64 | total := 0 65 | n, err := conn2.Write(data2) 66 | for { 67 | total += n 68 | if total < len(data2) { 69 | n, err = conn2.Write(data[total:len(data2)]) 70 | if err != nil { 71 | break 72 | } 73 | } else { 74 | break 75 | } 76 | } 77 | <-done 78 | } 79 | 80 | func TestRead(t *testing.T) { 81 | done := make(chan bool) 82 | secret := []byte("secret") 83 | data, data2 := makeTestBuf() 84 | pool := NewRecycler(4) 85 | ln, err := net.Listen("tcp", "127.0.0.1:9444") 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | defer ln.Close() 90 | go func() { 91 | conn, err := ln.Accept() 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | defer conn.Close() 96 | cipher := NewCipher("rc4", secret) 97 | cipherData := make([]byte, len(data)) 98 | cipher.encrypt(cipherData, data) 99 | total := 0 100 | n, err := conn.Write(cipherData) 101 | for { 102 | total += n 103 | if total < len(cipherData) { 104 | n, err = conn.Write(cipherData[total:len(cipherData)]) 105 | if err != nil { 106 | break 107 | } 108 | } else { 109 | break 110 | } 111 | } 112 | close(done) 113 | }() 114 | conn, err := net.Dial("tcp", "127.0.0.1:9444") 115 | if err != nil { 116 | t.Error(err) 117 | } 118 | conn2 := NewConn(conn, NewCipher("rc4", secret), pool) 119 | defer conn2.Close() 120 | buf := make([]byte, len(data2)) 121 | total := 0 122 | n, err := conn2.Read(buf) 123 | for { 124 | total += n 125 | if total < len(buf) { 126 | n, err = conn2.Read(buf[total:len(buf)]) 127 | if err != nil { 128 | break 129 | } 130 | } else { 131 | break 132 | } 133 | } 134 | if (bytes.Compare(data2, buf) != 0) { 135 | t.Fail() 136 | } 137 | <-done 138 | } 139 | -------------------------------------------------------------------------------- /src/tunnel/recycler.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "container/list" 5 | "time" 6 | ) 7 | 8 | type recyclerItem struct { 9 | when time.Time 10 | buf []byte 11 | } 12 | 13 | type recycler struct { 14 | q *list.List 15 | takeChan, giveChan chan []byte 16 | } 17 | 18 | func NewRecycler(size uint32) *recycler { 19 | r := &recycler{ 20 | q: new(list.List), 21 | takeChan: make(chan []byte), 22 | giveChan: make(chan []byte), 23 | } 24 | go r.cycle(size) 25 | return r 26 | } 27 | 28 | func (r *recycler) cycle(size uint32) { 29 | for { 30 | if r.q.Len() == 0 { 31 | // put to front so we always use the most recent buf 32 | r.q.PushFront(recyclerItem{when: time.Now(), buf: make([]byte, size)}) 33 | } 34 | i := r.q.Front() 35 | timeout := time.NewTimer(time.Minute) 36 | select { 37 | case b:= <-r.giveChan: 38 | timeout.Stop() 39 | r.q.PushFront(recyclerItem{when: time.Now(), buf: b}) 40 | case r.takeChan <- i.Value.(recyclerItem).buf: 41 | timeout.Stop() 42 | r.q.Remove(i) 43 | case <-timeout.C: 44 | i := r.q.Front() 45 | for i != nil { 46 | n := i.Next() 47 | if time.Since(i.Value.(recyclerItem).when) > time.Minute { 48 | r.q.Remove(i) 49 | i.Value = nil 50 | } 51 | i = n 52 | } 53 | } 54 | } 55 | } 56 | 57 | func (r *recycler) take() []byte { 58 | return <-r.takeChan 59 | } 60 | 61 | func (r *recycler) give(b []byte) { 62 | r.giveChan <- b 63 | } 64 | -------------------------------------------------------------------------------- /src/tunnel/tunnel.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "log" 7 | "time" 8 | "sync/atomic" 9 | ) 10 | 11 | type Tunnel struct { 12 | faddr, baddr *net.TCPAddr 13 | clientMode bool 14 | cryptoMethod string 15 | secret []byte 16 | sessionsCount int32 17 | pool *recycler 18 | } 19 | 20 | func NewTunnel(faddr, baddr string, clientMode bool, cryptoMethod, secret string, size uint32) *Tunnel { 21 | a1, err := net.ResolveTCPAddr("tcp", faddr) 22 | if err != nil { 23 | log.Fatalln("resolve frontend error:", err) 24 | } 25 | a2, err := net.ResolveTCPAddr("tcp", baddr) 26 | if err != nil { 27 | log.Fatalln("resolve backend error:", err) 28 | } 29 | return &Tunnel{ 30 | faddr: a1, 31 | baddr: a2, 32 | clientMode: clientMode, 33 | cryptoMethod: cryptoMethod, 34 | secret: []byte(secret), 35 | sessionsCount: 0, 36 | pool: NewRecycler(size), 37 | } 38 | } 39 | 40 | func (t *Tunnel) pipe(dst, src *Conn, c chan int64) { 41 | defer func() { 42 | dst.CloseWrite() 43 | src.CloseRead() 44 | }() 45 | n, err := io.Copy(dst, src) 46 | if err != nil { 47 | log.Print(err) 48 | } 49 | c <- n 50 | } 51 | 52 | func (t *Tunnel) transport(conn net.Conn) { 53 | start := time.Now() 54 | conn2, err := net.DialTCP("tcp", nil, t.baddr) 55 | if err != nil { 56 | log.Print(err) 57 | return 58 | } 59 | connectTime := time.Now().Sub(start) 60 | start = time.Now() 61 | cipher := NewCipher(t.cryptoMethod, t.secret) 62 | readChan := make(chan int64) 63 | writeChan := make(chan int64) 64 | var readBytes, writeBytes int64 65 | atomic.AddInt32(&t.sessionsCount, 1) 66 | var bconn, fconn *Conn 67 | if t.clientMode { 68 | fconn = NewConn(conn, nil, t.pool) 69 | bconn = NewConn(conn2, cipher, t.pool) 70 | } else { 71 | fconn = NewConn(conn, cipher, t.pool) 72 | bconn = NewConn(conn2, nil, t.pool) 73 | } 74 | go t.pipe(bconn, fconn, writeChan) 75 | go t.pipe(fconn, bconn, readChan) 76 | readBytes = <-readChan 77 | writeBytes = <-writeChan 78 | transferTime := time.Now().Sub(start) 79 | log.Printf("r:%d w:%d ct:%.3f t:%.3f [#%d]", readBytes, writeBytes, 80 | connectTime.Seconds(), transferTime.Seconds(), t.sessionsCount) 81 | atomic.AddInt32(&t.sessionsCount, -1) 82 | } 83 | 84 | func (t *Tunnel) Start() { 85 | ln, err := net.ListenTCP("tcp", t.faddr) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | defer ln.Close() 90 | 91 | for { 92 | conn, err := ln.AcceptTCP() 93 | if err != nil { 94 | log.Println("accept:", err) 95 | continue 96 | } 97 | go t.transport(conn) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/tunnel/tunnel_test.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "testing" 5 | "net" 6 | "bytes" 7 | "time" 8 | "fmt" 9 | ) 10 | 11 | func TestTunnelRC4(t *testing.T) { 12 | done := make(chan bool) 13 | data := []byte{1, 2, 3, 4} 14 | go backendServer(9480, data, done, t) 15 | b := NewTunnel("127.0.0.1:9446", "127.0.0.1:9480", false, "rc4", "secret", 4) 16 | f := NewTunnel("127.0.0.1:9447", "127.0.0.1:9446", true, "rc4", "secret", 4) 17 | go b.Start() 18 | go f.Start() 19 | // sleep to wait all servers start 20 | time.Sleep(100 * time.Millisecond) 21 | conn, err := net.Dial("tcp", "127.0.0.1:9447") 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | defer conn.Close() 26 | conn.Write(data) 27 | // wait for transmission complete 28 | time.Sleep(100 * time.Millisecond) 29 | close(done) 30 | } 31 | 32 | func TestTunnelAES256CFB(t *testing.T) { 33 | done := make(chan bool) 34 | data := []byte{1, 2, 3, 4} 35 | go backendServer(9481, data, done, t) 36 | b := NewTunnel("127.0.0.1:9448", "127.0.0.1:9481", false, "aes256cfb", "secret", 4) 37 | f := NewTunnel("127.0.0.1:9449", "127.0.0.1:9448", true, "aes256cfb", "secret", 4) 38 | go b.Start() 39 | go f.Start() 40 | // sleep to wait all servers start 41 | time.Sleep(100 * time.Millisecond) 42 | conn, err := net.Dial("tcp", "127.0.0.1:9449") 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | defer conn.Close() 47 | conn.Write(data) 48 | // wait for transmission complete 49 | time.Sleep(100 * time.Millisecond) 50 | close(done) 51 | } 52 | 53 | func backendServer(port int, data []byte, done chan bool, t *testing.T) { 54 | ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | defer ln.Close() 59 | conn, err := ln.Accept() 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | defer conn.Close() 64 | buf := make([]byte, len(data)) 65 | conn.Read(buf) 66 | if (bytes.Compare(buf, data) != 0) { 67 | t.Fail() 68 | } 69 | <-done 70 | } 71 | --------------------------------------------------------------------------------