From a163f066a6a93cd2d8b01509a4dba336c43c2036 Mon Sep 17 00:00:00 2001 From: Andy Wang Date: Mon, 6 Apr 2020 13:07:16 +0100 Subject: [PATCH] Refactor client config --- cmd/ck-client/ck-client.go | 317 ++++++++--------------------------- internal/client/TLS.go | 11 +- internal/client/auth.go | 29 +++- internal/client/connector.go | 88 ++++++++++ internal/client/piper.go | 144 ++++++++++++++++ internal/client/state.go | 202 +++++++++++++--------- internal/client/transport.go | 2 +- internal/client/websocket.go | 13 +- 8 files changed, 458 insertions(+), 348 deletions(-) create mode 100644 internal/client/connector.go create mode 100644 internal/client/piper.go diff --git a/cmd/ck-client/ck-client.go b/cmd/ck-client/ck-client.go index 9f9f2bf..9b55b07 100644 --- a/cmd/ck-client/ck-client.go +++ b/cmd/ck-client/ck-client.go @@ -4,220 +4,16 @@ package main import ( "encoding/base64" - "encoding/binary" "flag" "fmt" - "io" - "net" - "os" - "sync" - "sync/atomic" - "time" - "github.com/cbeuw/Cloak/internal/client" mux "github.com/cbeuw/Cloak/internal/multiplex" - "github.com/cbeuw/Cloak/internal/util" log "github.com/sirupsen/logrus" + "os" ) var version string -func makeSession(sta *client.State, isAdmin bool) *mux.Session { - log.Info("Attempting to start a new session") - if !isAdmin { - // sessionID is usergenerated. There shouldn't be a security concern because the scope of - // sessionID is limited to its UID. - quad := make([]byte, 4) - util.CryptoRandRead(quad) - atomic.StoreUint32(&sta.SessionID, binary.BigEndian.Uint32(quad)) - } - - d := net.Dialer{Control: protector, KeepAlive: sta.KeepAlive} - connsCh := make(chan net.Conn, sta.NumConn) - var _sessionKey atomic.Value - var wg sync.WaitGroup - for i := 0; i < sta.NumConn; i++ { - wg.Add(1) - go func() { - makeconn: - remoteConn, err := d.Dial("tcp", net.JoinHostPort(sta.RemoteHost, sta.RemotePort)) - if err != nil { - log.Errorf("Failed to establish new connections to remote: %v", err) - // TODO increase the interval if failed multiple times - time.Sleep(time.Second * 3) - goto makeconn - } - var sk []byte - remoteConn, sk, err = sta.Transport.PrepareConnection(sta, remoteConn) - if err != nil { - remoteConn.Close() - log.Errorf("Failed to prepare connection to remote: %v", err) - time.Sleep(time.Second * 3) - goto makeconn - } - _sessionKey.Store(sk) - connsCh <- remoteConn - wg.Done() - }() - } - wg.Wait() - log.Debug("All underlying connections established") - - sessionKey := _sessionKey.Load().([]byte) - obfuscator, err := mux.GenerateObfs(sta.EncryptionMethod, sessionKey, sta.Transport.HasRecordLayer()) - if err != nil { - log.Fatal(err) - } - - seshConfig := &mux.SessionConfig{ - Obfuscator: obfuscator, - Valve: nil, - UnitRead: sta.Transport.UnitReadFunc(), - Unordered: sta.Unordered, - } - sesh := mux.MakeSession(sta.SessionID, seshConfig) - - for i := 0; i < sta.NumConn; i++ { - conn := <-connsCh - sesh.AddConnection(conn) - } - - log.Infof("Session %v established", sta.SessionID) - return sesh -} - -func routeUDP(sta *client.State, adminUID []byte) { - var sesh *mux.Session - localUDPAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(sta.LocalHost, sta.LocalPort)) - if err != nil { - log.Fatal(err) - } -start: - localConn, err := net.ListenUDP("udp", localUDPAddr) - if err != nil { - log.Fatal(err) - } - var otherEnd atomic.Value - data := make([]byte, 10240) - i, oe, err := localConn.ReadFromUDP(data) - if err != nil { - log.Errorf("Failed to read first packet from proxy client: %v", err) - localConn.Close() - return - } - otherEnd.Store(oe) - - if sesh == nil || sesh.IsClosed() { - sesh = makeSession(sta, adminUID != nil) - } - log.Debugf("proxy local address %v", otherEnd.Load().(*net.UDPAddr).String()) - stream, err := sesh.OpenStream() - if err != nil { - log.Errorf("Failed to open stream: %v", err) - localConn.Close() - //localConnWrite.Close() - return - } - _, err = stream.Write(data[:i]) - if err != nil { - log.Errorf("Failed to write to stream: %v", err) - localConn.Close() - //localConnWrite.Close() - stream.Close() - return - } - - // stream to proxy - go func() { - buf := make([]byte, 16380) - for { - i, err := io.ReadAtLeast(stream, buf, 1) - if err != nil { - log.Print(err) - localConn.Close() - stream.Close() - break - } - _, err = localConn.WriteToUDP(buf[:i], otherEnd.Load().(*net.UDPAddr)) - if err != nil { - log.Print(err) - localConn.Close() - stream.Close() - break - } - } - }() - - // proxy to stream - buf := make([]byte, 16380) - if sta.Timeout != 0 { - localConn.SetReadDeadline(time.Now().Add(sta.Timeout)) - } - for { - if sta.Timeout != 0 { - localConn.SetReadDeadline(time.Now().Add(sta.Timeout)) - } - i, oe, err := localConn.ReadFromUDP(buf) - if err != nil { - localConn.Close() - stream.Close() - break - } - otherEnd.Store(oe) - _, err = stream.Write(buf[:i]) - if err != nil { - localConn.Close() - stream.Close() - break - } - } - goto start - -} - -func routeTCP(sta *client.State, adminUID []byte) { - tcpListener, err := net.Listen("tcp", net.JoinHostPort(sta.LocalHost, sta.LocalPort)) - if err != nil { - log.Fatal(err) - } - var sesh *mux.Session - for { - localConn, err := tcpListener.Accept() - if err != nil { - log.Fatal(err) - continue - } - if sesh == nil || sesh.IsClosed() { - sesh = makeSession(sta, adminUID != nil) - } - go func() { - data := make([]byte, 10240) - i, err := io.ReadAtLeast(localConn, data, 1) - if err != nil { - log.Errorf("Failed to read first packet from proxy client: %v", err) - localConn.Close() - return - } - stream, err := sesh.OpenStream() - if err != nil { - log.Errorf("Failed to open stream: %v", err) - localConn.Close() - return - } - _, err = stream.Write(data[:i]) - if err != nil { - log.Errorf("Failed to write to stream: %v", err) - localConn.Close() - stream.Close() - return - } - go util.Pipe(localConn, stream, 0) - util.Pipe(stream, localConn, sta.Timeout) - }() - } - -} - func main() { // Should be 127.0.0.1 to listen to a proxy client on this machine var localHost string @@ -234,16 +30,12 @@ func main() { log_init() + ssPluginMode := os.Getenv("SS_LOCAL_HOST") != "" + verbosity := flag.String("verbosity", "info", "verbosity level") - 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") + if ssPluginMode { config = os.Getenv("SS_PLUGIN_OPTIONS") - flag.Parse() // for verbosity only - } else { flag.StringVar(&localHost, "i", "127.0.0.1", "localHost: Cloak listens to proxy clients on this ip") flag.StringVar(&localPort, "l", "1984", "localPort: Cloak listens to proxy clients on this port") @@ -255,6 +47,9 @@ func main() { flag.StringVar(&b64AdminUID, "a", "", "adminUID: enter the adminUID to serve the admin api") askVersion := flag.Bool("v", false, "Print the version number") printUsage := flag.Bool("h", false, "Print this message") + + // commandline arguments overrides json + flag.Parse() if *askVersion { @@ -276,36 +71,62 @@ func main() { } log.SetLevel(lvl) - sta := &client.State{ - LocalHost: localHost, - LocalPort: localPort, - RemotePort: remotePort, - Now: time.Now, - } - - err = sta.ParseConfig(config) + rawConfig, err := client.ParseConfig(config) if err != nil { log.Fatal(err) } - if proxyMethod != "" { - sta.ProxyMethod = proxyMethod - } - - if remoteHost != "" { - sta.RemoteHost = remoteHost - } - - if os.Getenv("SS_LOCAL_HOST") != "" { - sta.ProxyMethod = "shadowsocks" + if ssPluginMode { + rawConfig.ProxyMethod = "shadowsocks" + // json takes precedence over environment variables + // i.e. if json field isn't empty, use that + if rawConfig.RemoteHost == "" { + rawConfig.RemoteHost = os.Getenv("SS_REMOTE_HOST") + } + if rawConfig.RemotePort == "" { + rawConfig.RemoteHost = os.Getenv("SS_REMOTE_PORT") + } + if rawConfig.LocalHost == "" { + rawConfig.LocalHost = os.Getenv("SS_LOCAL_HOST") + } + if rawConfig.LocalPort == "" { + rawConfig.LocalPort = os.Getenv("SS_LOCAL_PORT") + } + } else { + // commandline argument takes precedence over json + // if commandline argument is set, use commandline + flag.Visit(func(f *flag.Flag) { + // manually set ones + switch f.Name { + case "i": + rawConfig.LocalHost = localHost + case "l": + rawConfig.LocalPort = localPort + case "s": + rawConfig.RemoteHost = remoteHost + case "p": + rawConfig.RemotePort = remotePort + case "proxy": + rawConfig.ProxyMethod = proxyMethod + } + }) + // ones with default values + if rawConfig.LocalHost == "" { + rawConfig.LocalHost = localHost + } + if rawConfig.LocalPort == "" { + rawConfig.LocalPort = localPort + } + if rawConfig.RemotePort == "" { + rawConfig.RemotePort = remotePort + } } - if sta.LocalPort == "" { - log.Fatal("Must specify localPort") - } - if sta.RemoteHost == "" { - log.Fatal("Must specify remoteHost") + localConfig, remoteConfig, authInfo, err := rawConfig.SplitConfigs() + if err != nil { + log.Fatal(err) } + remoteConfig.Protector = protector var adminUID []byte if b64AdminUID != "" { @@ -315,26 +136,34 @@ func main() { } } + var seshMaker func() *mux.Session + if adminUID != nil { - log.Infof("API base is %v", net.JoinHostPort(sta.LocalHost, sta.LocalPort)) - sta.SessionID = 0 - sta.UID = adminUID - sta.NumConn = 1 + log.Infof("API base is %v", localConfig.LocalAddr) + authInfo.UID = adminUID + remoteConfig.NumConn = 1 + + seshMaker = func() *mux.Session { + return client.MakeSession(remoteConfig, authInfo, true) + } } else { var network string if udp { network = "UDP" - sta.Unordered = true + authInfo.Unordered = true } else { network = "TCP" - sta.Unordered = false + authInfo.Unordered = false + } + log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod) + seshMaker = func() *mux.Session { + return client.MakeSession(remoteConfig, authInfo, false) } - log.Infof("Listening on %v %v for %v client", network, net.JoinHostPort(sta.LocalHost, sta.LocalPort), sta.ProxyMethod) } if udp { - routeUDP(sta, adminUID) + client.RouteUDP(localConfig, seshMaker) } else { - routeTCP(sta, adminUID) + client.RouteTCP(localConfig, seshMaker) } } diff --git a/internal/client/TLS.go b/internal/client/TLS.go index 2960182..a344cc0 100644 --- a/internal/client/TLS.go +++ b/internal/client/TLS.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "github.com/cbeuw/Cloak/internal/util" "net" + "time" log "github.com/sirupsen/logrus" ) @@ -45,7 +46,7 @@ func addExtRec(typ []byte, data []byte) []byte { return ret } -func unmarshalAuthenticationInfo(ai authenticationPayload, serverName string) (ret clientHelloFields) { +func genStegClientHello(ai authenticationPayload, serverName string) (ret clientHelloFields) { // random is marshalled ephemeral pub key 32 bytes // The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare ret.random = ai.randPubKey[:] @@ -56,7 +57,7 @@ func unmarshalAuthenticationInfo(ai authenticationPayload, serverName string) (r } type DirectTLS struct { - Transport + browser } func (DirectTLS) HasRecordLayer() bool { return true } @@ -64,10 +65,10 @@ func (DirectTLS) UnitReadFunc() func(net.Conn, []byte) (int, error) { return uti // PrepareConnection handles the TLS handshake for a given conn and returns the sessionKey // if the server proceed with Cloak authentication -func (DirectTLS) PrepareConnection(sta *State, conn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) { +func (tls DirectTLS) PrepareConnection(authInfo *authInfo, conn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) { preparedConn = conn - payload, sharedSecret := makeAuthenticationPayload(sta, rand.Reader) - chOnly := sta.browser.composeClientHello(unmarshalAuthenticationInfo(payload, sta.ServerName)) + payload, sharedSecret := makeAuthenticationPayload(authInfo, rand.Reader, time.Now()) + chOnly := tls.browser.composeClientHello(genStegClientHello(payload, authInfo.MockDomain)) chWithRecordLayer := util.AddRecordLayer(chOnly, []byte{0x16}, []byte{0x03, 0x01}) _, err = preparedConn.Write(chWithRecordLayer) if err != nil { diff --git a/internal/client/auth.go b/internal/client/auth.go index 39deb6b..0fe126b 100644 --- a/internal/client/auth.go +++ b/internal/client/auth.go @@ -1,11 +1,12 @@ package client import ( + "crypto" "encoding/binary" "github.com/cbeuw/Cloak/internal/ecdh" "github.com/cbeuw/Cloak/internal/util" "io" - "sync/atomic" + "time" ) const ( @@ -17,9 +18,19 @@ type authenticationPayload struct { ciphertextWithTag [64]byte } +type authInfo struct { + UID []byte + SessionId uint32 + ProxyMethod string + EncryptionMethod byte + Unordered bool + ServerPubKey crypto.PublicKey + MockDomain string +} + // makeAuthenticationPayload generates the ephemeral key pair, calculates the shared secret, and then compose and // encrypt the authenticationPayload -func makeAuthenticationPayload(sta *State, randReader io.Reader) (ret authenticationPayload, sharedSecret []byte) { +func makeAuthenticationPayload(authInfo *authInfo, randReader io.Reader, time time.Time) (ret authenticationPayload, sharedSecret []byte) { /* Authentication data: +----------+----------------+---------------------+-------------+--------------+--------+------------+ @@ -32,17 +43,17 @@ func makeAuthenticationPayload(sta *State, randReader io.Reader) (ret authentica copy(ret.randPubKey[:], ecdh.Marshal(ephPub)) plaintext := make([]byte, 48) - copy(plaintext, sta.UID) - copy(plaintext[16:28], sta.ProxyMethod) - plaintext[28] = sta.EncryptionMethod - binary.BigEndian.PutUint64(plaintext[29:37], uint64(sta.Now().Unix())) - binary.BigEndian.PutUint32(plaintext[37:41], atomic.LoadUint32(&sta.SessionID)) + copy(plaintext, authInfo.UID) + copy(plaintext[16:28], authInfo.ProxyMethod) + plaintext[28] = authInfo.EncryptionMethod + binary.BigEndian.PutUint64(plaintext[29:37], uint64(time.Unix())) + binary.BigEndian.PutUint32(plaintext[37:41], authInfo.SessionId) - if sta.Unordered { + if authInfo.Unordered { plaintext[41] |= UNORDERED_FLAG } - sharedSecret = ecdh.GenerateSharedSecret(ephPv, sta.staticPub) + sharedSecret = ecdh.GenerateSharedSecret(ephPv, authInfo.ServerPubKey) ciphertextWithTag, _ := util.AESGCMEncrypt(ret.randPubKey[:12], sharedSecret, plaintext) copy(ret.ciphertextWithTag[:], ciphertextWithTag[:]) return diff --git a/internal/client/connector.go b/internal/client/connector.go new file mode 100644 index 0000000..583bbdb --- /dev/null +++ b/internal/client/connector.go @@ -0,0 +1,88 @@ +package client + +import ( + "encoding/binary" + "net" + "sync" + "sync/atomic" + "syscall" + "time" + + mux "github.com/cbeuw/Cloak/internal/multiplex" + "github.com/cbeuw/Cloak/internal/util" + log "github.com/sirupsen/logrus" +) + +type remoteConnConfig struct { + NumConn int + KeepAlive time.Duration + Protector func(string, string, syscall.RawConn) error + RemoteAddr string + Transport Transport +} + +func MakeSession(connConfig *remoteConnConfig, authInfo *authInfo, isAdmin bool) *mux.Session { + log.Info("Attempting to start a new session") + if !isAdmin { + // sessionID is usergenerated. There shouldn't be a security concern because the scope of + // sessionID is limited to its UID. + quad := make([]byte, 4) + util.CryptoRandRead(quad) + authInfo.SessionId = binary.BigEndian.Uint32(quad) + } else { + authInfo.SessionId = 0 + } + + d := net.Dialer{Control: connConfig.Protector, KeepAlive: connConfig.KeepAlive} + connsCh := make(chan net.Conn, connConfig.NumConn) + var _sessionKey atomic.Value + var wg sync.WaitGroup + for i := 0; i < connConfig.NumConn; i++ { + wg.Add(1) + go func() { + makeconn: + remoteConn, err := d.Dial("tcp", connConfig.RemoteAddr) + if err != nil { + log.Errorf("Failed to establish new connections to remote: %v", err) + // TODO increase the interval if failed multiple times + time.Sleep(time.Second * 3) + goto makeconn + } + var sk []byte + remoteConn, sk, err = connConfig.Transport.PrepareConnection(authInfo, remoteConn) + if err != nil { + remoteConn.Close() + log.Errorf("Failed to prepare connection to remote: %v", err) + time.Sleep(time.Second * 3) + goto makeconn + } + _sessionKey.Store(sk) + connsCh <- remoteConn + wg.Done() + }() + } + wg.Wait() + log.Debug("All underlying connections established") + + sessionKey := _sessionKey.Load().([]byte) + obfuscator, err := mux.GenerateObfs(authInfo.EncryptionMethod, sessionKey, connConfig.Transport.HasRecordLayer()) + if err != nil { + log.Fatal(err) + } + + seshConfig := &mux.SessionConfig{ + Obfuscator: obfuscator, + Valve: nil, + UnitRead: connConfig.Transport.UnitReadFunc(), + Unordered: authInfo.Unordered, + } + sesh := mux.MakeSession(authInfo.SessionId, seshConfig) + + for i := 0; i < connConfig.NumConn; i++ { + conn := <-connsCh + sesh.AddConnection(conn) + } + + log.Infof("Session %v established", authInfo.SessionId) + return sesh +} diff --git a/internal/client/piper.go b/internal/client/piper.go new file mode 100644 index 0000000..02c2973 --- /dev/null +++ b/internal/client/piper.go @@ -0,0 +1,144 @@ +package client + +import ( + "io" + "net" + "sync/atomic" + "time" + + mux "github.com/cbeuw/Cloak/internal/multiplex" + "github.com/cbeuw/Cloak/internal/util" + log "github.com/sirupsen/logrus" +) + +func RouteUDP(localConfig *localConnConfig, newSeshFunc func() *mux.Session) { + var sesh *mux.Session + localUDPAddr, err := net.ResolveUDPAddr("udp", localConfig.LocalAddr) + if err != nil { + log.Fatal(err) + } +start: + localConn, err := net.ListenUDP("udp", localUDPAddr) + if err != nil { + log.Fatal(err) + } + var otherEnd atomic.Value + data := make([]byte, 10240) + i, oe, err := localConn.ReadFromUDP(data) + if err != nil { + log.Errorf("Failed to read first packet from proxy client: %v", err) + localConn.Close() + return + } + otherEnd.Store(oe) + + if sesh == nil || sesh.IsClosed() { + sesh = newSeshFunc() + } + log.Debugf("proxy local address %v", otherEnd.Load().(*net.UDPAddr).String()) + stream, err := sesh.OpenStream() + if err != nil { + log.Errorf("Failed to open stream: %v", err) + localConn.Close() + //localConnWrite.Close() + return + } + _, err = stream.Write(data[:i]) + if err != nil { + log.Errorf("Failed to write to stream: %v", err) + localConn.Close() + //localConnWrite.Close() + stream.Close() + return + } + + // stream to proxy + go func() { + buf := make([]byte, 16380) + for { + i, err := io.ReadAtLeast(stream, buf, 1) + if err != nil { + log.Print(err) + localConn.Close() + stream.Close() + break + } + _, err = localConn.WriteToUDP(buf[:i], otherEnd.Load().(*net.UDPAddr)) + if err != nil { + log.Print(err) + localConn.Close() + stream.Close() + break + } + } + }() + + // proxy to stream + buf := make([]byte, 16380) + if localConfig.Timeout != 0 { + localConn.SetReadDeadline(time.Now().Add(localConfig.Timeout)) + } + for { + if localConfig.Timeout != 0 { + localConn.SetReadDeadline(time.Now().Add(localConfig.Timeout)) + } + i, oe, err := localConn.ReadFromUDP(buf) + if err != nil { + localConn.Close() + stream.Close() + break + } + otherEnd.Store(oe) + _, err = stream.Write(buf[:i]) + if err != nil { + localConn.Close() + stream.Close() + break + } + } + goto start + +} + +func RouteTCP(localConfig *localConnConfig, newSeshFunc func() *mux.Session) { + tcpListener, err := net.Listen("tcp", localConfig.LocalAddr) + if err != nil { + log.Fatal(err) + } + var sesh *mux.Session + for { + localConn, err := tcpListener.Accept() + if err != nil { + log.Fatal(err) + continue + } + if sesh == nil || sesh.IsClosed() { + sesh = newSeshFunc() + } + go func() { + data := make([]byte, 10240) + i, err := io.ReadAtLeast(localConn, data, 1) + if err != nil { + log.Errorf("Failed to read first packet from proxy client: %v", err) + localConn.Close() + return + } + stream, err := sesh.OpenStream() + if err != nil { + log.Errorf("Failed to open stream: %v", err) + localConn.Close() + return + } + _, err = stream.Write(data[:i]) + if err != nil { + log.Errorf("Failed to write to stream: %v", err) + localConn.Close() + stream.Close() + return + } + go util.Pipe(localConn, stream, 0) + util.Pipe(stream, localConn, localConfig.Timeout) + }() + } + +} diff --git a/internal/client/state.go b/internal/client/state.go index c96f141..fd1cb43 100644 --- a/internal/client/state.go +++ b/internal/client/state.go @@ -1,11 +1,10 @@ package client import ( - "crypto" "encoding/json" - "errors" + "fmt" "io/ioutil" - "strconv" + "net" "strings" "time" @@ -14,54 +13,51 @@ import ( ) // rawConfig represents the fields in the config json file +// nullable means if it's empty, a default value will be chosen in SplitConfigs +// jsonOptional means if the json's empty, its value will be set from environment variables or commandline args +// but it mustn't be empty when SplitConfigs is called type rawConfig struct { ServerName string ProxyMethod string EncryptionMethod string UID []byte PublicKey []byte - BrowserSig string - Transport string NumConn int - StreamTimeout int - KeepAlive int - RemoteHost string - RemotePort int + LocalHost string // jsonOptional + LocalPort string // jsonOptional + RemoteHost string // jsonOptional + RemotePort string // jsonOptional + //TODO: udp + + // defaults set in SplitConfigs + BrowserSig string // nullable + Transport string // nullable + StreamTimeout int // nullable + KeepAlive int // nullable } -// State stores the parsed configuration fields -type State struct { - LocalHost string - LocalPort string - RemoteHost string - RemotePort string - Unordered bool - - Transport Transport - - SessionID uint32 - UID []byte - - staticPub crypto.PublicKey - Now func() time.Time // for easier testing - browser browser - - ProxyMethod string - EncryptionMethod byte - ServerName string - NumConn int - Timeout time.Duration - KeepAlive time.Duration +type localConnConfig struct { + LocalAddr string + Timeout time.Duration } // semi-colon separated value. This is for Android plugin options func ssvToJson(ssv string) (ret []byte) { + elem := func(val string, lst []string) bool { + for _, v := range lst { + if val == v { + return true + } + } + return false + } unescape := func(s string) string { r := strings.Replace(s, `\\`, `\`, -1) r = strings.Replace(r, `\=`, `=`, -1) r = strings.Replace(r, `\;`, `;`, -1) return r } + unquoted := []string{"NumConn", "StreamTimeout", "KeepAlive"} lines := strings.Split(unescape(ssv), ";") ret = []byte("{") for _, ln := range lines { @@ -73,7 +69,7 @@ func ssvToJson(ssv string) (ret []byte) { value := sp[1] // JSON doesn't like quotation marks around int and bool // This is extremely ugly but it's still better than writing a tokeniser - if key == "NumConn" || key == "Unordered" || key == "StreamTimeout" { + if elem(key, unquoted) { ret = append(ret, []byte(`"`+key+`":`+value+`,`)...) } else { ret = append(ret, []byte(`"`+key+`":"`+value+`",`)...) @@ -84,8 +80,7 @@ func ssvToJson(ssv string) (ret []byte) { 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) { +func ParseConfig(conf string) (raw *rawConfig, err error) { var content []byte // Checking if it's a path to json or a ssv string if strings.Contains(conf, ";") && strings.Contains(conf, "=") { @@ -93,75 +88,116 @@ func (sta *State) ParseConfig(conf string) (err error) { } else { content, err = ioutil.ReadFile(conf) if err != nil { - return err + return } } - var preParse rawConfig - err = json.Unmarshal(content, &preParse) + + raw = new(rawConfig) + err = json.Unmarshal(content, &raw) if err != nil { - return err + return } + return +} - switch strings.ToLower(preParse.EncryptionMethod) { +func (raw *rawConfig) SplitConfigs() (local *localConnConfig, remote *remoteConnConfig, auth *authInfo, err error) { + nullErr := func(field string) (local *localConnConfig, remote *remoteConnConfig, auth *authInfo, err error) { + err = fmt.Errorf("%v cannot be empty", field) + return + } + + auth = new(authInfo) + if raw.ServerName == "" { + return nullErr("ServerName") + } + auth.MockDomain = raw.ServerName + if raw.ProxyMethod == "" { + return nullErr("ServerName") + } + auth.ProxyMethod = raw.ProxyMethod + if len(raw.UID) == 0 { + return nullErr("UID") + } + auth.UID = raw.UID + + // static public key + if len(raw.PublicKey) == 0 { + return nullErr("PublicKey") + } + pub, ok := ecdh.Unmarshal(raw.PublicKey) + if !ok { + err = fmt.Errorf("failed to unmarshal Public key") + return + } + auth.ServerPubKey = pub + + // Encryption method + switch strings.ToLower(raw.EncryptionMethod) { case "plain": - sta.EncryptionMethod = mux.E_METHOD_PLAIN + auth.EncryptionMethod = mux.E_METHOD_PLAIN case "aes-gcm": - sta.EncryptionMethod = mux.E_METHOD_AES_GCM + auth.EncryptionMethod = mux.E_METHOD_AES_GCM case "chacha20-poly1305": - sta.EncryptionMethod = mux.E_METHOD_CHACHA20_POLY1305 + auth.EncryptionMethod = mux.E_METHOD_CHACHA20_POLY1305 default: - return errors.New("Unknown encryption method") + err = fmt.Errorf("unknown encryption method %v", raw.EncryptionMethod) + return } - switch strings.ToLower(preParse.BrowserSig) { - case "chrome": - sta.browser = &Chrome{} - case "firefox": - sta.browser = &Firefox{} - default: - return errors.New("unsupported browser signature") + remote = new(remoteConnConfig) + if raw.RemoteHost == "" { + return nullErr("RemoteHost") + } + if raw.RemotePort == "" { + return nullErr("RemotePort") } + remote.RemoteAddr = net.JoinHostPort(raw.RemoteHost, raw.RemotePort) + if raw.NumConn == 0 { + return nullErr("NumConn") + } + remote.NumConn = raw.NumConn - switch strings.ToLower(preParse.Transport) { - case "direct": - sta.Transport = DirectTLS{} + // Transport and (if TLS mode), browser + switch strings.ToLower(raw.Transport) { case "cdn": - sta.Transport = WSOverTLS{} + remote.Transport = WSOverTLS{remote.RemoteAddr} + case "direct": + fallthrough default: - sta.Transport = DirectTLS{} + var browser browser + switch strings.ToLower(raw.BrowserSig) { + case "firefox": + browser = &Firefox{} + case "chrome": + fallthrough + default: + browser = &Chrome{} + } + remote.Transport = DirectTLS{browser} } - sta.RemoteHost = preParse.RemoteHost - sta.ProxyMethod = preParse.ProxyMethod - sta.ServerName = preParse.ServerName - sta.NumConn = preParse.NumConn - if preParse.StreamTimeout == 0 { - sta.Timeout = 300 * time.Second + // KeepAlive + if raw.KeepAlive <= 0 { + remote.KeepAlive = -1 } else { - sta.Timeout = time.Duration(preParse.StreamTimeout) * time.Second + remote.KeepAlive = remote.KeepAlive * time.Second } - if preParse.KeepAlive <= 0 { - sta.KeepAlive = -1 - } else { - sta.KeepAlive = time.Duration(preParse.KeepAlive) * time.Second - } - sta.UID = preParse.UID - pub, ok := ecdh.Unmarshal(preParse.PublicKey) - if !ok { - return errors.New("Failed to unmarshal Public key") - } - sta.staticPub = pub + local = new(localConnConfig) - // OPTIONAL: set RemotePort via JSON - // if RemotePort is specified in the JSON we overwrite sta.RemotePort - // if not, don't do anything, since sta.RemotePort is already initialised in ck-client.go - if preParse.RemotePort != 0 { - // basic validity check - if preParse.RemotePort >= 1 && preParse.RemotePort <= 65535 { - sta.RemotePort = strconv.Itoa(preParse.RemotePort) - } + if raw.LocalHost == "" { + return nullErr("LocalHost") + } + if raw.LocalPort == "" { + return nullErr("LocalPort") + } + local.LocalAddr = net.JoinHostPort(raw.LocalHost, raw.LocalPort) + // stream no write timeout + if raw.StreamTimeout == 0 { + local.Timeout = 300 * time.Second + } else { + local.Timeout = time.Duration(raw.StreamTimeout) * time.Second } - return nil + return } diff --git a/internal/client/transport.go b/internal/client/transport.go index 411c3de..7f983c6 100644 --- a/internal/client/transport.go +++ b/internal/client/transport.go @@ -3,7 +3,7 @@ package client import "net" type Transport interface { - PrepareConnection(*State, net.Conn) (net.Conn, []byte, error) + PrepareConnection(*authInfo, net.Conn) (net.Conn, []byte, error) HasRecordLayer() bool UnitReadFunc() func(net.Conn, []byte) (int, error) } diff --git a/internal/client/websocket.go b/internal/client/websocket.go index 8269890..51c3e73 100644 --- a/internal/client/websocket.go +++ b/internal/client/websocket.go @@ -10,35 +10,36 @@ import ( "net" "net/http" "net/url" + "time" utls "github.com/refraction-networking/utls" ) type WSOverTLS struct { - Transport + cdnDomainPort string } func (WSOverTLS) HasRecordLayer() bool { return false } func (WSOverTLS) UnitReadFunc() func(net.Conn, []byte) (int, error) { return util.ReadWebSocket } -func (WSOverTLS) PrepareConnection(sta *State, conn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) { +func (ws WSOverTLS) PrepareConnection(authInfo *authInfo, cdnConn net.Conn) (preparedConn net.Conn, sessionKey []byte, err error) { utlsConfig := &utls.Config{ - ServerName: sta.ServerName, + ServerName: authInfo.MockDomain, InsecureSkipVerify: true, } - uconn := utls.UClient(conn, utlsConfig, utls.HelloChrome_Auto) + uconn := utls.UClient(cdnConn, utlsConfig, utls.HelloChrome_Auto) err = uconn.Handshake() preparedConn = uconn if err != nil { return } - u, err := url.Parse("ws://" + sta.RemoteHost + ":" + sta.RemotePort) //TODO IPv6 + u, err := url.Parse("ws://" + ws.cdnDomainPort) if err != nil { return preparedConn, nil, fmt.Errorf("failed to parse ws url: %v", err) } - payload, sharedSecret := makeAuthenticationPayload(sta, rand.Reader) + payload, sharedSecret := makeAuthenticationPayload(authInfo, rand.Reader, time.Now()) header := http.Header{} header.Add("hidden", base64.StdEncoding.EncodeToString(append(payload.randPubKey[:], payload.ciphertextWithTag[:]...))) c, _, err := websocket.NewClient(preparedConn, u, header, 16480, 16480)