From a3c3c5dc07a47c7b5841f1004907d0e927abcd9e Mon Sep 17 00:00:00 2001 From: Andy Wang Date: Sat, 31 Aug 2019 18:01:39 +0100 Subject: [PATCH] Client side plain websocket --- cmd/ck-client/ck-client.go | 2 +- example_config/ckclient.json | 1 + go.mod | 1 + internal/client/TLS.go | 6 ++- internal/client/auth.go | 10 +++-- internal/client/state.go | 12 ++++++ internal/client/transport.go | 7 +++ internal/client/websocket.go | 83 ++++++++++++++++++++++++++++++++++++ 8 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 internal/client/transport.go create mode 100644 internal/client/websocket.go diff --git a/cmd/ck-client/ck-client.go b/cmd/ck-client/ck-client.go index 55e5a13..bf9f9a4 100644 --- a/cmd/ck-client/ck-client.go +++ b/cmd/ck-client/ck-client.go @@ -53,7 +53,7 @@ func makeSession(sta *client.State, isAdmin bool) *mux.Session { time.Sleep(time.Second * 3) goto makeconn } - sk, err := client.PrepareConnection(sta, remoteConn) + sk, err := sta.Transport.PrepareConnection(sta, remoteConn) if err != nil { remoteConn.Close() log.Errorf("Failed to prepare connection to remote: %v", err) diff --git a/example_config/ckclient.json b/example_config/ckclient.json index eb0fd21..8abcf8f 100644 --- a/example_config/ckclient.json +++ b/example_config/ckclient.json @@ -1,4 +1,5 @@ { + "Transport": "TLS", "ProxyMethod":"shadowsocks", "EncryptionMethod":"plain", "UID":"5nneblJy6lniPJfr81LuYQ==", diff --git a/go.mod b/go.mod index 1b638d6..742e412 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( github.com/boltdb/bolt v1.3.1 github.com/gorilla/mux v1.7.3 + github.com/gorilla/websocket v1.4.1 github.com/juju/ratelimit v1.0.1 github.com/kr/pretty v0.1.0 // indirect github.com/sirupsen/logrus v1.4.2 diff --git a/internal/client/TLS.go b/internal/client/TLS.go index 56f2dfb..e7d8470 100644 --- a/internal/client/TLS.go +++ b/internal/client/TLS.go @@ -37,9 +37,13 @@ func addExtRec(typ []byte, data []byte) []byte { return ret } +type TLS struct { + Transport +} + // PrepareConnection handles the TLS handshake for a given conn and returns the sessionKey // if the server proceed with Cloak authentication -func PrepareConnection(sta *State, conn net.Conn) (sessionKey []byte, err error) { +func (*TLS) PrepareConnection(sta *State, conn net.Conn) (sessionKey []byte, err error) { hd, sharedSecret := makeHiddenData(sta) chOnly := sta.browser.composeClientHello(hd) chWithRecordLayer := util.AddRecordLayer(chOnly, []byte{0x16}, []byte{0x03, 0x01}) diff --git a/internal/client/auth.go b/internal/client/auth.go index 6e5df04..aa23eb2 100644 --- a/internal/client/auth.go +++ b/internal/client/auth.go @@ -13,10 +13,11 @@ const ( ) type chHiddenData struct { - chRandom []byte - chSessionId []byte - chX25519KeyShare []byte - chExtSNI []byte + rawCiphertextWithTag []byte + chRandom []byte + chSessionId []byte + chX25519KeyShare []byte + chExtSNI []byte } // makeHiddenData generates the ephemeral key pair, calculates the shared secret, and then compose and @@ -49,6 +50,7 @@ func makeHiddenData(sta *State) (ret chHiddenData, sharedSecret []byte) { sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub) nonce := ret.chRandom[0:12] ciphertextWithTag, _ := util.AESGCMEncrypt(nonce, sharedSecret, plaintext) + ret.rawCiphertextWithTag = ciphertextWithTag ret.chSessionId = ciphertextWithTag[0:32] ret.chX25519KeyShare = ciphertextWithTag[32:64] ret.chExtSNI = makeServerName(sta.ServerName) diff --git a/internal/client/state.go b/internal/client/state.go index 23f57e0..bb663c1 100644 --- a/internal/client/state.go +++ b/internal/client/state.go @@ -21,6 +21,7 @@ type rawConfig struct { UID string PublicKey string BrowserSig string + Transport string NumConn int StreamTimeout int } @@ -33,6 +34,8 @@ type State struct { RemotePort string Unordered bool + Transport Transport + SessionID uint32 UID []byte @@ -115,6 +118,15 @@ func (sta *State) ParseConfig(conf string) (err error) { return errors.New("unsupported browser signature") } + switch strings.ToLower(preParse.Transport) { + case "tls": + sta.Transport = &TLS{} + case "websocket": + sta.Transport = &WebSocket{} + default: + sta.Transport = &TLS{} + } + sta.ProxyMethod = preParse.ProxyMethod sta.ServerName = preParse.ServerName sta.NumConn = preParse.NumConn diff --git a/internal/client/transport.go b/internal/client/transport.go new file mode 100644 index 0000000..7cd7a66 --- /dev/null +++ b/internal/client/transport.go @@ -0,0 +1,7 @@ +package client + +import "net" + +type Transport interface { + PrepareConnection(*State, net.Conn) ([]byte, error) +} diff --git a/internal/client/websocket.go b/internal/client/websocket.go new file mode 100644 index 0000000..9ad1875 --- /dev/null +++ b/internal/client/websocket.go @@ -0,0 +1,83 @@ +package client + +import ( + "encoding/base64" + "errors" + "github.com/cbeuw/Cloak/internal/util" + "net" + "net/http" + "net/url" + "time" + + "github.com/gorilla/websocket" +) + +type WebSocketConn struct { + c *websocket.Conn +} + +func (ws *WebSocketConn) Write(data []byte) (int, error) { + err := ws.c.WriteMessage(websocket.BinaryMessage, data) + if err != nil { + return 0, err + } else { + return len(data), nil + } +} + +func (ws *WebSocketConn) Read(buf []byte) (int, error) { + _, r, err := ws.c.NextReader() + if err != nil { + return 0, err + } + return r.Read(buf) +} + +func (ws *WebSocketConn) Close() error { return ws.c.Close() } +func (ws *WebSocketConn) LocalAddr() net.Addr { return ws.c.LocalAddr() } +func (ws *WebSocketConn) RemoteAddr() net.Addr { return ws.c.RemoteAddr() } +func (ws *WebSocketConn) SetDeadline(t time.Time) error { + err := ws.c.SetReadDeadline(t) + if err != nil { + return err + } + err = ws.c.SetWriteDeadline(t) + if err != nil { + return err + } + return nil +} +func (ws *WebSocketConn) SetReadDeadline(t time.Time) error { return ws.c.SetReadDeadline(t) } +func (ws *WebSocketConn) SetWriteDeadline(t time.Time) error { return ws.c.SetWriteDeadline(t) } + +type WebSocket struct { + Transport +} + +func (WebSocket) PrepareConnection(sta *State, conn net.Conn) (sessionKey []byte, err error) { + u, err := url.Parse("ws://" + sta.RemoteHost + ":" + sta.RemotePort) //TODO IPv6 + if err != nil { + return nil, err + } + + hd, sharedSecret := makeHiddenData(sta) + header := http.Header{} + header.Add("hidden", base64.StdEncoding.EncodeToString(hd.rawCiphertextWithTag)) + c, resp, err := websocket.NewClient(conn, u, header, 16480, 16480) + if err != nil { + return nil, err + } + + reply, err := base64.StdEncoding.DecodeString(resp.Header.Get("reply")) + if err != nil { + return nil, err + } + + if len(reply) != 60 { + return nil, errors.New("reply must be 60 bytes") + } + sessionKey, err = util.AESGCMDecrypt(reply[:12], sharedSecret, reply[12:]) + + conn = &WebSocketConn{c: c} + return +}