diff --git a/cmd/ck-client/ck-client.go b/cmd/ck-client/ck-client.go new file mode 100644 index 0000000..8c2067d --- /dev/null +++ b/cmd/ck-client/ck-client.go @@ -0,0 +1,178 @@ +// +build go1.11 + +package main + +import ( + "flag" + "fmt" + "io" + "log" + "net" + "os" + "time" + + "github.com/cbeuw/Cloak/internal/client" + "github.com/cbeuw/Cloak/internal/client/TLS" + mux "github.com/cbeuw/Cloak/internal/multiplex" + "github.com/cbeuw/Cloak/internal/util" +) + +var version string + +func pipe(dst io.ReadWriteCloser, src io.ReadWriteCloser) { + buf := make([]byte, 20480) + for { + i, err := src.Read(buf) + if err != nil { + go dst.Close() + go src.Close() + return + } + _, err = dst.Write(buf[:i]) + if err != nil { + go dst.Close() + go src.Close() + return + } + } +} + +// This establishes a connection with ckserver and performs a handshake +func makeRemoteConn(sta *client.State) net.Conn { + + d := net.Dialer{Control: protector} + + clientHello := TLS.ComposeInitHandshake(sta) + remoteConn, err := d.Dial("tcp", sta.SS_REMOTE_HOST+":"+sta.SS_REMOTE_PORT) + if err != nil { + log.Printf("Connecting to remote: %v\n", err) + return nil + } + _, err = remoteConn.Write(clientHello) + if err != nil { + log.Printf("Sending ClientHello: %v\n", err) + return nil + } + + // Three discarded messages: ServerHello, ChangeCipherSpec and Finished + discardBuf := make([]byte, 1024) + for c := 0; c < 3; c++ { + _, err = util.ReadTillDrain(remoteConn, discardBuf) + if err != nil { + log.Printf("Reading discarded message %v: %v\n", c, err) + return nil + } + } + + reply := TLS.ComposeReply() + _, err = remoteConn.Write(reply) + if err != nil { + log.Printf("Sending reply to remote: %v\n", err) + return nil + } + + return remoteConn + +} + +func main() { + // Should be 127.0.0.1 to listen to ss-local on this machine + var localHost string + // server_port in ss config, ss sends data on loopback using this port + var localPort string + // The ip of the proxy server + var remoteHost string + // The proxy port,should be 443 + var remotePort string + var pluginOpts string + + log.SetFlags(log.LstdFlags | log.Lshortfile) + + log_init() + + if os.Getenv("SS_LOCAL_HOST") != "" { + localHost = os.Getenv("SS_LOCAL_HOST") + localPort = os.Getenv("SS_LOCAL_PORT") + remoteHost = os.Getenv("SS_REMOTE_HOST") + remotePort = os.Getenv("SS_REMOTE_PORT") + pluginOpts = os.Getenv("SS_PLUGIN_OPTIONS") + } else { + localHost = "127.0.0.1" + flag.StringVar(&localPort, "l", "", "localPort: same as server_port in ss config, the plugin listens to SS using this") + flag.StringVar(&remoteHost, "s", "", "remoteHost: IP of your proxy server") + flag.StringVar(&remotePort, "p", "443", "remotePort: proxy port, should be 443") + flag.StringVar(&pluginOpts, "c", "ckclient.json", "pluginOpts: path to ckclient.json or options seperated with semicolons") + askVersion := flag.Bool("v", false, "Print the version number") + printUsage := flag.Bool("h", false, "Print this message") + flag.Parse() + + if *askVersion { + fmt.Printf("ck-client %s\n", version) + return + } + + if *printUsage { + flag.Usage() + return + } + + log.Printf("Starting standalone mode. Listening for ss on %v:%v\n", localHost, localPort) + } + + sta := &client.State{ + SS_LOCAL_HOST: localHost, + SS_LOCAL_PORT: localPort, + SS_REMOTE_HOST: remoteHost, + SS_REMOTE_PORT: remotePort, + Now: time.Now, + } + err := sta.ParseConfig(pluginOpts) + if err != nil { + log.Fatal(err) + } + + if sta.SS_LOCAL_PORT == "" { + log.Fatal("Must specify localPort") + } + if sta.SS_REMOTE_HOST == "" { + log.Fatal("Must specify remoteHost") + } + if sta.TicketTimeHint == 0 { + log.Fatal("TicketTimeHint cannot be empty or 0") + } + + initRemoteConn := makeRemoteConn(sta) + + obfs := util.MakeObfs(sta.SID) + deobfs := util.MakeDeobfs(sta.SID) + // TODO: where to put obfs deobfs and rtd? + sesh := mux.MakeSession(0, initRemoteConn, obfs, deobfs, util.ReadTillDrain) + + for i := 0; i < sta.NumConn-1; i++ { + go func() { + conn := makeRemoteConn(sta) + sesh.AddConnection(conn) + }() + } + + listener, err := net.Listen("tcp", sta.SS_LOCAL_HOST+":"+sta.SS_LOCAL_PORT) + if err != nil { + log.Fatal(err) + } + for { + ssConn, err := listener.Accept() + if err != nil { + log.Println(err) + continue + } + go func() { + stream, err := sesh.OpenStream() + if err != nil { + ssConn.Close() + } + go pipe(ssConn, stream) + pipe(stream, ssConn) + }() + } + +} diff --git a/cmd/ck-client/log.go b/cmd/ck-client/log.go new file mode 100644 index 0000000..4fc5dec --- /dev/null +++ b/cmd/ck-client/log.go @@ -0,0 +1,6 @@ +// +build !android + +package main + +func log_init() { +} diff --git a/cmd/ck-client/log_android.go b/cmd/ck-client/log_android.go new file mode 100644 index 0000000..d46a789 --- /dev/null +++ b/cmd/ck-client/log_android.go @@ -0,0 +1,85 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build android + +package main + +/* +To view the log output run: +adb logcat GoLog:I *:S +*/ + +// Android redirects stdout and stderr to /dev/null. +// As these are common debugging utilities in Go, +// we redirect them to logcat. +// +// Unfortunately, logcat is line oriented, so we must buffer. + +/* +#cgo LDFLAGS: -landroid -llog + +#include +#include +#include +*/ +import "C" + +import ( + "bufio" + "log" + "os" + "unsafe" +) + +var ( + ctag = C.CString("cloak") +) + +type infoWriter struct{} + +func (infoWriter) Write(p []byte) (n int, err error) { + cstr := C.CString(string(p)) + C.__android_log_write(C.ANDROID_LOG_INFO, ctag, cstr) + C.free(unsafe.Pointer(cstr)) + return len(p), nil +} + +func lineLog(f *os.File, priority C.int) { + const logSize = 1024 // matches android/log.h. + r := bufio.NewReaderSize(f, logSize) + for { + line, _, err := r.ReadLine() + str := string(line) + if err != nil { + str += " " + err.Error() + } + cstr := C.CString(str) + C.__android_log_write(priority, ctag, cstr) + C.free(unsafe.Pointer(cstr)) + if err != nil { + break + } + } +} + +func log_init() { + log.SetOutput(infoWriter{}) + // android logcat includes all of log.LstdFlags + log.SetFlags(log.Flags() &^ log.LstdFlags) + + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + os.Stderr = w + go lineLog(r, C.ANDROID_LOG_ERROR) + + r, w, err = os.Pipe() + if err != nil { + panic(err) + } + os.Stdout = w + go lineLog(r, C.ANDROID_LOG_INFO) +} diff --git a/cmd/ck-client/protector.go b/cmd/ck-client/protector.go new file mode 100644 index 0000000..3af0dde --- /dev/null +++ b/cmd/ck-client/protector.go @@ -0,0 +1,9 @@ +// +build !android + +package main + +import "syscall" + +func protector(string, string, syscall.RawConn) error { + return nil +} diff --git a/cmd/ck-client/protector_android.go b/cmd/ck-client/protector_android.go new file mode 100644 index 0000000..7f635b9 --- /dev/null +++ b/cmd/ck-client/protector_android.go @@ -0,0 +1,121 @@ +// +build android +package main + +// Stolen from https://github.com/shadowsocks/overture/blob/shadowsocks/core/utils/utils_android.go + +/* +#include +#include +#include +#include +#include +#include +#include +#include + +#define ANCIL_FD_BUFFER(n) \ + struct { \ + struct cmsghdr h; \ + int fd[n]; \ + } + + int + ancil_send_fds_with_buffer(int sock, const int *fds, unsigned n_fds, void *buffer) + { + struct msghdr msghdr; + char nothing = '!'; + struct iovec nothing_ptr; + struct cmsghdr *cmsg; + int i; + + nothing_ptr.iov_base = ¬hing; + nothing_ptr.iov_len = 1; + msghdr.msg_name = NULL; + msghdr.msg_namelen = 0; + msghdr.msg_iov = ¬hing_ptr; + msghdr.msg_iovlen = 1; + msghdr.msg_flags = 0; + msghdr.msg_control = buffer; + msghdr.msg_controllen = sizeof(struct cmsghdr) + sizeof(int) * n_fds; + cmsg = CMSG_FIRSTHDR(&msghdr); + cmsg->cmsg_len = msghdr.msg_controllen; + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + for(i = 0; i < n_fds; i++) + ((int *)CMSG_DATA(cmsg))[i] = fds[i]; + return(sendmsg(sock, &msghdr, 0) >= 0 ? 0 : -1); + } + + int + ancil_send_fd(int sock, int fd) + { + ANCIL_FD_BUFFER(1) buffer; + + return(ancil_send_fds_with_buffer(sock, &fd, 1, &buffer)); + } + + void + set_timeout(int sock) + { + struct timeval tv; + tv.tv_sec = 3; + tv.tv_usec = 0; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(struct timeval)); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(struct timeval)); + } +*/ +import "C" + +import ( + "log" + "syscall" +) + +// In Android, once an app starts the VpnService, all outgoing traffic are routed by the system +// to the VPN app. In our case, the VPN app is ss-local. Our outgoing traffic to ck-server +// will be routed back to ss-local which creates an infinite loop. +// +// The Android system provides an API VpnService.protect(int socketFD) +// This tells the system to bypass the socket around the VPN. +func protector(network string, address string, c syscall.RawConn) error { + log.Println("Using Android VPN mode.") + fn := func(s uintptr) { + fd := int(s) + path := "protect_path" + + socket, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + log.Println(err) + return + } + + defer syscall.Close(socket) + + C.set_timeout(C.int(socket)) + + err = syscall.Connect(socket, &syscall.SockaddrUnix{Name: path}) + if err != nil { + log.Println(err) + return + } + + C.ancil_send_fd(C.int(socket), C.int(fd)) + + dummy := []byte{1} + n, err := syscall.Read(socket, dummy) + if err != nil { + log.Println(err) + return + } + if n != 1 { + log.Println("Failed to protect fd: ", fd) + return + } + } + + if err := c.Control(fn); err != nil { + return err + } + + return nil +} diff --git a/internal/client/TLS/TLS.go b/internal/client/TLS/TLS.go new file mode 100644 index 0000000..9acab54 --- /dev/null +++ b/internal/client/TLS/TLS.go @@ -0,0 +1,70 @@ +package TLS + +import ( + "encoding/binary" + "github.com/cbeuw/Cloak/internal/client" + "github.com/cbeuw/Cloak/internal/util" + "time" +) + +type browser interface { + composeExtensions() + composeClientHello() +} + +func makeServerName(sta *client.State) []byte { + serverName := sta.ServerName + serverNameListLength := make([]byte, 2) + binary.BigEndian.PutUint16(serverNameListLength, uint16(len(serverName)+3)) + serverNameType := []byte{0x00} // host_name + serverNameLength := make([]byte, 2) + binary.BigEndian.PutUint16(serverNameLength, uint16(len(serverName))) + ret := make([]byte, 2+1+2+len(serverName)) + copy(ret[0:2], serverNameListLength) + copy(ret[2:3], serverNameType) + copy(ret[3:5], serverNameLength) + copy(ret[5:], serverName) + return ret +} + +func makeNullBytes(length int) []byte { + ret := make([]byte, length) + for i := 0; i < length; i++ { + ret[i] = 0x00 + } + return ret +} + +// addExtensionRecord, add type, length to extension data +func addExtRec(typ []byte, data []byte) []byte { + length := make([]byte, 2) + binary.BigEndian.PutUint16(length, uint16(len(data))) + ret := make([]byte, 2+2+len(data)) + copy(ret[0:2], typ) + copy(ret[2:4], length) + copy(ret[4:], data) + return ret +} + +// ComposeInitHandshake composes ClientHello with record layer +func ComposeInitHandshake(sta *client.State) []byte { + var ch []byte + switch sta.MaskBrowser { + case "chrome": + ch = (&chrome{}).composeClientHello(sta) + case "firefox": + ch = (&firefox{}).composeClientHello(sta) + default: + panic("Unsupported browser:" + sta.MaskBrowser) + } + return util.AddRecordLayer(ch, []byte{0x16}, []byte{0x03, 0x01}) +} + +// ComposeReply composes RL+ChangeCipherSpec+RL+Finished +func ComposeReply() []byte { + TLS12 := []byte{0x03, 0x03} + ccsBytes := util.AddRecordLayer([]byte{0x01}, []byte{0x14}, TLS12) + finished := util.PsudoRandBytes(40, time.Now().UnixNano()) + fBytes := util.AddRecordLayer(finished, []byte{0x16}, TLS12) + return append(ccsBytes, fBytes...) +} diff --git a/internal/client/TLS/chrome.go b/internal/client/TLS/chrome.go new file mode 100644 index 0000000..78b23b7 --- /dev/null +++ b/internal/client/TLS/chrome.go @@ -0,0 +1,82 @@ +// Chrome 64 + +package TLS + +import ( + "encoding/hex" + "math/rand" + "time" + + "github.com/cbeuw/Cloak/internal/client" + "github.com/cbeuw/Cloak/internal/util" +) + +type chrome struct { + browser +} + +func (c *chrome) composeExtensions(sta *client.State) []byte { + // see https://tools.ietf.org/html/draft-davidben-tls-grease-01 + // This is exclusive to chrome. + makeGREASE := func() []byte { + rand.Seed(time.Now().UnixNano()) + sixteenth := rand.Intn(16) + monoGREASE := byte(sixteenth*16 + 0xA) + doubleGREASE := []byte{monoGREASE, monoGREASE} + return doubleGREASE + } + + makeSupportedGroups := func() []byte { + suppGroupListLen := []byte{0x00, 0x08} + ret := make([]byte, 2+8) + copy(ret[0:2], suppGroupListLen) + copy(ret[2:4], makeGREASE()) + copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18}) + return ret + } + + var ext [14][]byte + ext[0] = addExtRec(makeGREASE(), nil) // First GREASE + ext[1] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info + ext[2] = addExtRec([]byte{0x00, 0x00}, makeServerName(sta)) // server name indication + ext[3] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret + ext[4] = addExtRec([]byte{0x00, 0x23}, client.MakeSessionTicket(sta)) // Session tickets + sigAlgo, _ := hex.DecodeString("0012040308040401050308050501080606010201") + ext[5] = addExtRec([]byte{0x00, 0x0d}, sigAlgo) // Signature Algorithms + ext[6] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request + ext[7] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp + APLN, _ := hex.DecodeString("000c02683208687474702f312e31") + ext[8] = addExtRec([]byte{0x00, 0x10}, APLN) // app layer proto negotiation + ext[9] = addExtRec([]byte{0x75, 0x50}, nil) // channel id + ext[10] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats + ext[11] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups + ext[12] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE + ext[13] = addExtRec([]byte{0x00, 0x15}, makeNullBytes(110-len(ext[2]))) // padding + var ret []byte + for i := 0; i < 14; i++ { + ret = append(ret, ext[i]...) + } + return ret +} + +func (c *chrome) composeClientHello(sta *client.State) []byte { + var clientHello [12][]byte + clientHello[0] = []byte{0x01} // handshake type + clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508 + clientHello[2] = []byte{0x03, 0x03} // client version + clientHello[3] = client.MakeRandomField(sta) // random + clientHello[4] = []byte{0x20} // session id length 32 + clientHello[5] = util.PsudoRandBytes(32, sta.Now().UnixNano()) // session id + clientHello[6] = []byte{0x00, 0x1c} // cipher suites length 28 + cipherSuites, _ := hex.DecodeString("2a2ac02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a") + clientHello[7] = cipherSuites // cipher suites + clientHello[8] = []byte{0x01} // compression methods length 1 + clientHello[9] = []byte{0x00} // compression methods + clientHello[10] = []byte{0x01, 0x97} // extensions length 407 + clientHello[11] = c.composeExtensions(sta) // extensions + var ret []byte + for i := 0; i < 12; i++ { + ret = append(ret, clientHello[i]...) + } + return ret +} diff --git a/internal/client/TLS/firefox.go b/internal/client/TLS/firefox.go new file mode 100644 index 0000000..19f671f --- /dev/null +++ b/internal/client/TLS/firefox.go @@ -0,0 +1,57 @@ +// Firefox 58 +package TLS + +import ( + "encoding/hex" + + "github.com/cbeuw/Cloak/internal/client" + "github.com/cbeuw/Cloak/internal/util" +) + +type firefox struct { + browser +} + +func (f *firefox) composeExtensions(sta *client.State) []byte { + var ext [10][]byte + ext[0] = addExtRec([]byte{0x00, 0x00}, makeServerName(sta)) // server name indication + ext[1] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret + ext[2] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info + suppGroup, _ := hex.DecodeString("0008001d001700180019") + ext[3] = addExtRec([]byte{0x00, 0x0a}, suppGroup) // supported groups + ext[4] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats + ext[5] = addExtRec([]byte{0x00, 0x23}, client.MakeSessionTicket(sta)) // Session tickets + APLN, _ := hex.DecodeString("000c02683208687474702f312e31") + ext[6] = addExtRec([]byte{0x00, 0x10}, APLN) // app layer proto negotiation + ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request + sigAlgo, _ := hex.DecodeString("001604030503060308040805080604010501060102030201") + ext[8] = addExtRec([]byte{0x00, 0x0d}, sigAlgo) // Signature Algorithms + ext[9] = addExtRec([]byte{0x00, 0x15}, makeNullBytes(121-len(ext[0]))) // padding + var ret []byte + for i := 0; i < 10; i++ { + ret = append(ret, ext[i]...) + } + return ret +} + +func (f *firefox) composeClientHello(sta *client.State) []byte { + var clientHello [12][]byte + clientHello[0] = []byte{0x01} // handshake type + clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508 + clientHello[2] = []byte{0x03, 0x03} // client version + clientHello[3] = client.MakeRandomField(sta) // random + clientHello[4] = []byte{0x20} // session id length 32 + clientHello[5] = util.PsudoRandBytes(32, sta.Now().UnixNano()) // session id + clientHello[6] = []byte{0x00, 0x1e} // cipher suites length 28 + cipherSuites, _ := hex.DecodeString("c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a") + clientHello[7] = cipherSuites // cipher suites + clientHello[8] = []byte{0x01} // compression methods length 1 + clientHello[9] = []byte{0x00} // compression methods + clientHello[10] = []byte{0x01, 0x95} // extensions length 405 + clientHello[11] = f.composeExtensions(sta) // extensions + var ret []byte + for i := 0; i < 12; i++ { + ret = append(ret, clientHello[i]...) + } + return ret +} diff --git a/internal/client/auth.go b/internal/client/auth.go new file mode 100644 index 0000000..fd5a00a --- /dev/null +++ b/internal/client/auth.go @@ -0,0 +1,59 @@ +package client + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "github.com/cbeuw/Cloak/internal/util" + "github.com/cbeuw/ecies" +) + +func MakeRandomField(sta *State) []byte { + + t := make([]byte, 8) + binary.BigEndian.PutUint64(t, uint64(sta.Now().Unix()/12*60*60)) + rand := util.PsudoRandBytes(16, sta.Now().UnixNano()) + preHash := make([]byte, 56) + copy(preHash[0:32], sta.SID) + copy(preHash[32:40], t) + copy(preHash[40:56], rand) + h := sha256.New() + h.Write(preHash) + ret := make([]byte, 32) + copy(ret[0:16], rand) + copy(ret[16:32], h.Sum(nil)[0:16]) + return ret +} + +func MakeSessionTicket(sta *State) []byte { + t := make([]byte, 8) + binary.BigEndian.PutUint64(t, uint64(sta.Now().Unix()/int64(sta.TicketTimeHint))) + plain := make([]byte, 40) + copy(plain, sta.SID) + copy(plain[32:], t) + // With the default settings (P256, AES128, SHA256) of the ecies package, len(ct)==153. + // + // ciphertext is composed of 3 parts: marshalled X and Y coordinates on the curve, + // iv+ciphertext of the block cipher (aes128 in this case), + // and the hmac which is 32 bytes because it's sha256 + // + // The marshalling is done by crypto/elliptic.Marshal. According to the code, + // the size after marshall is 65 + // + // IV is 16 bytes. The size of ciphertext is equal to the plaintext, which is 40, + // that is 32 bytes of SID + 8 bytes of timestamp/tickettimehint. + // 16+40 = 56 + // + // Then the hmac is 32 bytes + // + // 65+56+32=153 + ct, _ := ecies.Encrypt(rand.Reader, sta.Pub, plain, nil, nil) + sessionTicket := make([]byte, 192) + // The reason for ct[1:] is that, the first byte of ct is always 0x04 + // This is specified in the section 4.3.6 of ANSI X9.62 (the uncompressed form). + // This is a flag that is useless to us and it will expose our pattern + // (because the sessionTicket isn't fully random anymore). Therefore we drop it. + copy(sessionTicket, ct[1:]) + copy(sessionTicket[152:], util.PsudoRandBytes(40, sta.Now().UnixNano())) + return sessionTicket +} diff --git a/internal/client/state.go b/internal/client/state.go new file mode 100644 index 0000000..8302a48 --- /dev/null +++ b/internal/client/state.go @@ -0,0 +1,110 @@ +package client + +import ( + "crypto/elliptic" + "encoding/base64" + "encoding/json" + "errors" + "github.com/cbeuw/ecies" + "io/ioutil" + "strings" + "time" +) + +type rawConfig struct { + ServerName string + Key string + TicketTimeHint int + MaskBrowser string + NumConn int +} + +// State stores global variables +type State struct { + SS_LOCAL_HOST string + SS_LOCAL_PORT string + SS_REMOTE_HOST string + SS_REMOTE_PORT string + Now func() time.Time + SID []byte + Pub *ecies.PublicKey + TicketTimeHint int + ServerName string + MaskBrowser string + NumConn int +} + +// semi-colon separated value. This is for Android plugin options +func ssvToJson(ssv string) (ret []byte) { + unescape := func(s string) string { + r := strings.Replace(s, "\\\\", "\\", -1) + r = strings.Replace(r, "\\=", "=", -1) + r = strings.Replace(r, "\\;", ";", -1) + return r + } + lines := strings.Split(unescape(ssv), ";") + ret = []byte("{") + for _, ln := range lines { + if ln == "" { + break + } + sp := strings.SplitN(ln, "=", 2) + key := sp[0] + value := sp[1] + // JSON doesn't like quotation marks around int + // Yes this is extremely ugly but it's still better than writing a tokeniser + if key == "TicketTimeHint" || key == "NumConn" { + ret = append(ret, []byte("\""+key+"\":"+value+",")...) + } else { + ret = append(ret, []byte("\""+key+"\":\""+value+"\",")...) + } + } + ret = ret[:len(ret)-1] // remove the last comma + ret = append(ret, '}') + return ret +} + +// ParseConfig parses the config (either a path to json or Android config) into a State variable +func (sta *State) ParseConfig(conf string) (err error) { + var content []byte + if strings.Contains(conf, ";") && strings.Contains(conf, "=") { + content = ssvToJson(conf) + } else { + content, err = ioutil.ReadFile(conf) + if err != nil { + return err + } + } + var preParse rawConfig + err = json.Unmarshal(content, &preParse) + if err != nil { + return err + } + sta.ServerName = preParse.ServerName + sta.TicketTimeHint = preParse.TicketTimeHint + sta.MaskBrowser = preParse.MaskBrowser + sid, pub, err := parseKey(preParse.Key) + if err != nil { + return errors.New("Failed to parse Key: " + err.Error()) + } + sta.SID = sid + sta.Pub = pub + return nil +} + +func parseKey(b64 string) ([]byte, *ecies.PublicKey, error) { + b, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + return nil, nil, err + } + sid := b[0:32] + marshalled := b[32:] + x, y := elliptic.Unmarshal(ecies.DefaultCurve, marshalled) + pub := &ecies.PublicKey{ + X: x, + Y: y, + Curve: ecies.DefaultCurve, + Params: ecies.ParamsFromCurve(ecies.DefaultCurve), + } + return sid, pub, nil +} diff --git a/internal/multiplex/frame.go b/internal/multiplex/frame.go index 49328e9..32b5b2a 100644 --- a/internal/multiplex/frame.go +++ b/internal/multiplex/frame.go @@ -3,8 +3,8 @@ package multiplex import () type Frame struct { - StreamID uint32 - Seq uint32 - ClosedStreamID uint32 - Payload []byte + StreamID uint32 + Seq uint32 + ClosingStreamID uint32 + Payload []byte } diff --git a/internal/multiplex/session.go b/internal/multiplex/session.go index ef3d32f..af1992c 100644 --- a/internal/multiplex/session.go +++ b/internal/multiplex/session.go @@ -7,8 +7,9 @@ import ( const ( // Copied from smux - errBrokenPipe = "broken pipe" - acceptBacklog = 1024 + errBrokenPipe = "broken pipe" + errRepeatStreamClosing = "trying to close a closed stream" + acceptBacklog = 1024 closeBacklog = 512 ) @@ -41,19 +42,27 @@ type Session struct { } // TODO: put this in main maybe? -func MakeSession(id int, conns []net.Conn) *Session { +// 1 conn is needed to make a session +func MakeSession(id int, conn net.Conn, obfs func(*Frame) []byte, deobfs func([]byte) *Frame, obfsedReader func(net.Conn, []byte) (int, error)) *Session { sesh := &Session{ id: id, + obfs: obfs, + deobfs: deobfs, + obfsedReader: obfsedReader, nextStreamID: 0, streams: make(map[uint32]*Stream), acceptCh: make(chan *Stream, acceptBacklog), closeQCh: make(chan uint32, closeBacklog), } - sesh.sb = makeSwitchboard(conns, sesh) + sesh.sb = makeSwitchboard(conn, sesh) sesh.sb.run() return sesh } +func (sesh *Session) AddConnection(conn net.Conn) { + sesh.sb.newConnCh <- conn +} + func (sesh *Session) OpenStream() (*Stream, error) { sesh.nextStreamIDM.Lock() id := sesh.nextStreamID diff --git a/internal/multiplex/stream.go b/internal/multiplex/stream.go index dac338d..228d80b 100644 --- a/internal/multiplex/stream.go +++ b/internal/multiplex/stream.go @@ -29,6 +29,9 @@ type Stream struct { nextSendSeqM sync.Mutex nextSendSeq uint32 + + closingM sync.Mutex + closing bool } func makeStream(id uint32, sesh *Session) *Stream { @@ -73,9 +76,9 @@ func (stream *Stream) Write(in []byte) (n int, err error) { } f := &Frame{ - StreamID: stream.id, - Seq: stream.nextSendSeq, - ClosedStreamID: closingID, + StreamID: stream.id, + Seq: stream.nextSendSeq, + ClosingStreamID: closingID, } copy(f.Payload, in) @@ -91,6 +94,13 @@ func (stream *Stream) Write(in []byte) (n int, err error) { } func (stream *Stream) Close() error { + // Because closing a closed channel causes panic + stream.closingM.Lock() + defer stream.closingM.Unlock() + if stream.closing { + return errors.New(errRepeatStreamClosing) + } + stream.closing = true stream.session.delStream(stream.id) close(stream.die) close(stream.sortedBufCh) diff --git a/internal/multiplex/switchboard.go b/internal/multiplex/switchboard.go index af83099..4afdebc 100644 --- a/internal/multiplex/switchboard.go +++ b/internal/multiplex/switchboard.go @@ -18,6 +18,7 @@ type switchboard struct { // For telling dispatcher how many bytes have been sent after Connection.send. sentNotifyCh chan *sentNotifier dispatCh chan []byte + newConnCh chan net.Conn } // Some data comes from a Stream to be sent through one of the many @@ -43,28 +44,27 @@ func (a byQ) Less(i, j int) bool { return a[i].sendQueue < a[j].sendQueue } -func makeSwitchboard(conns []net.Conn, sesh *Session) *switchboard { +// It takes at least 1 conn to start a switchboard +func makeSwitchboard(conn net.Conn, sesh *Session) *switchboard { sb := &switchboard{ session: sesh, ces: []*connEnclave{}, sentNotifyCh: make(chan *sentNotifier, sentNotifyBacklog), dispatCh: make(chan []byte, dispatchBacklog), } - for _, c := range conns { - ce := &connEnclave{ - sb: sb, - remoteConn: c, - sendQueue: 0, - } - sb.ces = append(sb.ces, ce) + ce := &connEnclave{ + sb: sb, + remoteConn: conn, + sendQueue: 0, } + sb.ces = append(sb.ces, ce) return sb } func (sb *switchboard) run() { - go startDispatcher() - go startDeplexer() + go sb.startDispatcher() + go sb.startDeplexer() } // Everytime after a remoteConn sends something, it constructs this struct @@ -86,6 +86,7 @@ func (ce *connEnclave) send(data []byte) { } // Dispatcher sends data coming from a stream to a remote connection +// I used channels here because I didn't want to use mutex func (sb *switchboard) startDispatcher() { for { select { @@ -96,6 +97,14 @@ func (sb *switchboard) startDispatcher() { case notified := <-sb.sentNotifyCh: notified.ce.sendQueue -= notified.sent sort.Sort(byQ(sb.ces)) + case conn := <-sb.newConnCh: + newCe := &connEnclave{ + sb: sb, + remoteConn: conn, + sendQueue: 0, + } + sb.ces = append(sb.ces, newCe) + sort.Sort(byQ(sb.ces)) } } } @@ -111,6 +120,7 @@ func (sb *switchboard) startDeplexer() { if !sb.session.isStream(frame.StreamID) { sb.session.addStream(frame.StreamID) } + sb.session.getStream(frame.ClosingStreamID).Close() sb.session.getStream(frame.StreamID).recvNewFrame(frame) } }() diff --git a/internal/util/obfs.go b/internal/util/obfs.go new file mode 100644 index 0000000..9e0e292 --- /dev/null +++ b/internal/util/obfs.go @@ -0,0 +1,72 @@ +package util + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/binary" + + mux "github.com/cbeuw/Cloak/internal/multiplex" +) + +func encrypt(iv []byte, key []byte, plaintext []byte) []byte { + block, _ := aes.NewCipher(key) + ciphertext := make([]byte, len(plaintext)) + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext, plaintext) + return ciphertext +} + +func decrypt(iv []byte, key []byte, ciphertext []byte) []byte { + ret := make([]byte, len(ciphertext)) + copy(ret, ciphertext) // Because XORKeyStream is inplace, but we don't want the input to be changed + block, _ := aes.NewCipher(key) + stream := cipher.NewCFBDecrypter(block, iv) + stream.XORKeyStream(ret, ret) + // ret is now plaintext + return ret +} + +func MakeObfs(key []byte) func(*mux.Frame) []byte { + obfs := func(f *mux.Frame) []byte { + header := make([]byte, 12) + binary.BigEndian.PutUint32(header[0:4], f.StreamID) + binary.BigEndian.PutUint32(header[4:8], f.Seq) + binary.BigEndian.PutUint32(header[8:12], f.ClosingStreamID) + // header: [StreamID 4 bytes][Seq 4 bytes][ClosingStreamID 4 bytes] + plaintext := make([]byte, 12+len(f.Payload)-16) + copy(plaintext[0:12], header) + copy(plaintext[12:], f.Payload[16:]) + // plaintext: [header 12 bytes][Payload[16:]] + iv := f.Payload[0:16] + ciphertext := encrypt(iv, key, plaintext) + obfsed := make([]byte, 16+len(ciphertext)) + copy(obfsed[0:16], iv) + copy(obfsed[16:], ciphertext) + // obfsed: [iv 16 bytes][ciphertext] + ret := AddRecordLayer(obfsed, []byte{0x17}, []byte{0x03, 0x03}) + return ret + } + return obfs +} + +func MakeDeobfs(key []byte) func([]byte) *mux.Frame { + deobfs := func(in []byte) *mux.Frame { + peeled := PeelRecordLayer(in) + plaintext := decrypt(peeled[0:16], key, peeled[16:]) + // plaintext: [header 12 bytes][Payload[16:]] + streamID := binary.BigEndian.Uint32(plaintext[0:4]) + seq := binary.BigEndian.Uint32(plaintext[4:8]) + closingStreamID := binary.BigEndian.Uint32(plaintext[8:12]) + payload := make([]byte, len(plaintext)-12) + copy(payload[0:16], peeled[0:16]) + copy(payload[16:], plaintext[12:]) + ret := &mux.Frame{ + StreamID: streamID, + Seq: seq, + ClosingStreamID: closingStreamID, + Payload: payload, + } + return ret + } + return deobfs +} diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..bac43ed --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,105 @@ +package util + +import ( + "crypto/rand" + "encoding/binary" + "errors" + "io" + "math/big" + prand "math/rand" + "net" + "time" +) + +// BtoInt converts a byte slice into int in Big Endian order +// Uint methods from binary package can be used, but they are messy +func BtoInt(b []byte) int { + var mult uint = 1 + var sum uint + length := uint(len(b)) + var i uint + for i = 0; i < length; i++ { + sum += uint(b[i]) * (mult << ((length - i - 1) * 8)) + } + return int(sum) +} + +// CryptoRandBytes generates a byte slice filled with cryptographically secure random bytes +func CryptoRandBytes(length int) []byte { + byteMax := big.NewInt(int64(256)) + ret := make([]byte, length) + for i := 0; i < length; i++ { + randInt, _ := rand.Int(rand.Reader, byteMax) + randByte := byte(randInt.Int64()) + ret[i] = randByte + } + return ret +} + +// PsudoRandBytes returns a byte slice filled with psudorandom bytes generated by the seed +func PsudoRandBytes(length int, seed int64) []byte { + prand.Seed(seed) + ret := make([]byte, length) + for i := 0; i < length; i++ { + randByte := byte(prand.Intn(256)) + ret[i] = randByte + } + return ret +} + +// ReadTillDrain reads TLS data according to its record layer +func ReadTillDrain(conn net.Conn, buffer []byte) (n int, err error) { + // TCP is a stream. Multiple TLS messages can arrive at the same time, + // a single message can also be segmented due to MTU of the IP layer. + // This function guareentees a single TLS message to be read and everything + // else is left in the buffer. + i, err := io.ReadFull(conn, buffer[:5]) + if err != nil { + return + } + + dataLength := BtoInt(buffer[3:5]) + left := dataLength + readPtr := 5 + + conn.SetReadDeadline(time.Now().Add(3 * time.Second)) + for left != 0 { + if readPtr > len(buffer) || readPtr+left > len(buffer) { + err = errors.New("Reading TLS message: actual size greater than header's specification") + return + } + // If left > buffer size (i.e. our message got segmented), the entire MTU is read + // if left = buffer size, the entire buffer is all there left to read + // if left < buffer size (i.e. multiple messages came together), + // only the message we want is read + i, err = io.ReadFull(conn, buffer[readPtr:readPtr+left]) + if err != nil { + return + } + left -= i + readPtr += i + } + conn.SetReadDeadline(time.Time{}) + + n = 5 + dataLength + buffer = buffer[:n] + return +} + +// AddRecordLayer adds record layer to data +func AddRecordLayer(input []byte, typ []byte, ver []byte) []byte { + length := make([]byte, 2) + binary.BigEndian.PutUint16(length, uint16(len(input))) + ret := make([]byte, 5+len(input)) + copy(ret[0:1], typ) + copy(ret[1:3], ver) + copy(ret[3:5], length) + copy(ret[5:], input) + return ret +} + +// PeelRecordLayer peels off the record layer +func PeelRecordLayer(data []byte) []byte { + ret := data[5:] + return ret +}