Compare commits

...

19 Commits

Author SHA1 Message Date
Andy Wang 018994bfd1
Rebase fixup 2 months ago
Andy Wang 0a6846fbfc
Refactor singleplex handling 2 months ago
Andy Wang f30141b388
Refactor client Config 2 months ago
Andy Wang 2aa49ce543
Separate Client out into its own library package 2 months ago
Andy Wang fe78c7b713
Document Client Config options 2 months ago
Andy Wang 4029763123
Refactor client transport modules 2 months ago
Andy Wang 896fd16938
Refactor Browser fingerprint modules 2 months ago
Andy Wang 4fbf387bbf
Refactor client for PTSpec Go API 2 months ago
Andy Wang de1c7600c1
Update dependencies 2 months ago
Andy Wang 767716b9be
Merge pull request #251 from cbeuw/renovate/github.com-refraction-networking-utls-1.x
Update module github.com/refraction-networking/utls to v1.6.4
2 months ago
renovate[bot] 1cc4a1f928
Update module github.com/refraction-networking/utls to v1.6.4 2 months ago
Andy Wang 82687d4419
Merge pull request #256 from BANanaD3V/master
Update go mod so it builds on nix
2 months ago
Nikita 6b08af0c18
Update go mod so it builds on nix 3 months ago
Andy Wang c48a8800d6
Remove old utls 4 months ago
renovate[bot] c5b31de753
Configure Renovate (#248)
* Add renovate.json

* Renovate only utls

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Andy Wang <cbeuw.andy@gmail.com>
4 months ago
Andy Wang b9907c2e18
Disable codecov check 4 months ago
Andy Wang 6417e3393d
Use utls for ClientHello fingerprint
Close #223
4 months ago
Andy Wang b3ec1ab3bc
Make server respond with a TLS 1.3 cipher suite 4 months ago
Andy Wang eca5f13936
Remove WriteTo from recvBuffer to prevent blocking on external Writer.
Fixes #229
7 months ago

@ -123,7 +123,7 @@ upstream proxy server. Zero or negative value disables it. Default is 0 (disable
`UID` is your UID in base64. `UID` is your UID in base64.
`Transport` can be either `direct` or `CDN`. If the server host wishes you to connect to it directly, use `direct`. If `Transport` can be either `direct` or `CDN`. If the server host wishes you to connect to it directly, use `direct`. If
instead a CDN is used, use `CDN`. instead a CDN is used, use `CDN`. Defaults to `direct`
`PublicKey` is the static curve25519 public key in base64, given by the server admin. `PublicKey` is the static curve25519 public key in base64, given by the server admin.
@ -131,7 +131,7 @@ instead a CDN is used, use `CDN`.
server's `ProxyBook` exactly. server's `ProxyBook` exactly.
`EncryptionMethod` is the name of the encryption algorithm you want Cloak to use. Options are `plain`, `aes-256-gcm` ( `EncryptionMethod` is the name of the encryption algorithm you want Cloak to use. Options are `plain`, `aes-256-gcm` (
synonymous to `aes-gcm`), `aes-128-gcm`, and `chacha20-poly1305`. Note: Cloak isn't intended to provide transport synonymous to `aes-gcm`), `aes-128-gcm`, and `chacha20-poly1305`. Defaults to `aes-256-gcm` if empty. Note: Cloak isn't intended to provide transport
security. The point of encryption is to hide fingerprints of proxy protocols and render the payload statistically security. The point of encryption is to hide fingerprints of proxy protocols and render the payload statistically
random-like. **You may only leave it as `plain` if you are certain that your underlying proxy tool already provides BOTH random-like. **You may only leave it as `plain` if you are certain that your underlying proxy tool already provides BOTH
encryption and authentication (via AEAD or similar techniques).** encryption and authentication (via AEAD or similar techniques).**
@ -141,7 +141,7 @@ match `RedirAddr` in the server's configuration, a major site the censor allows,
`AlternativeNames` is an array used alongside `ServerName` to shuffle between different ServerNames for every new `AlternativeNames` is an array used alongside `ServerName` to shuffle between different ServerNames for every new
connection. **This may conflict with `CDN` Transport mode** if the CDN provider prohibits domain fronting and rejects connection. **This may conflict with `CDN` Transport mode** if the CDN provider prohibits domain fronting and rejects
the alternative domains. the alternative domains. Default is empty.
Example: Example:
@ -165,8 +165,8 @@ requests under specific url path are forwarded.
`NumConn` is the amount of underlying TCP connections you want to use. The default of 4 should be appropriate for most `NumConn` is the amount of underlying TCP connections you want to use. The default of 4 should be appropriate for most
people. Setting it too high will hinder the performance. Setting it to 0 will disable connection multiplexing and each people. Setting it too high will hinder the performance. Setting it to 0 will disable connection multiplexing and each
TCP connection will spawn a separate short-lived session that will be closed after it is terminated. This makes it TCP connection will spawn a separate short-lived session that will be closed after it is terminated. This maybe useful
behave like GoQuiet. This maybe useful for people with unstable connections. for people with unstable connections.
`BrowserSig` is the browser you want to **appear** to be using. It's not relevant to the browser you are actually using. `BrowserSig` is the browser you want to **appear** to be using. It's not relevant to the browser you are actually using.
Currently, `chrome`, `firefox` and `safari` are supported. Currently, `chrome`, `firefox` and `safari` are supported.

@ -8,19 +8,22 @@ import (
"encoding/binary" "encoding/binary"
"flag" "flag"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/cli_client"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client"
"net" "net"
"os" "os"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/client"
mux "github.com/cbeuw/Cloak/internal/multiplex"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var version string var version string
func main() { func main() {
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
// Should be 127.0.0.1 to listen to a proxy client on this machine // Should be 127.0.0.1 to listen to a proxy client on this machine
var localHost string var localHost string
// port used by proxy clients to communicate with cloak client // port used by proxy clients to communicate with cloak client
@ -75,16 +78,13 @@ func main() {
log.Info("Starting standalone mode") log.Info("Starting standalone mode")
} }
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
lvl, err := log.ParseLevel(*verbosity) lvl, err := log.ParseLevel(*verbosity)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.SetLevel(lvl) log.SetLevel(lvl)
rawConfig, err := client.ParseConfig(config) rawConfig, err := cli_client.ParseConfig(config)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -139,7 +139,7 @@ func main() {
} }
} }
localConfig, remoteConfig, authInfo, err := rawConfig.ProcessRawConfig(common.RealWorldState) localConfig, remoteConfig, authInfo, err := rawConfig.ProcessCLIConfig(common.RealWorldState)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -152,7 +152,7 @@ func main() {
} }
} }
var seshMaker func() *mux.Session var seshMaker func() *client.CloakClient
d := &net.Dialer{Control: protector, KeepAlive: remoteConfig.KeepAlive} d := &net.Dialer{Control: protector, KeepAlive: remoteConfig.KeepAlive}
@ -162,8 +162,8 @@ func main() {
authInfo.SessionId = 0 authInfo.SessionId = 0
remoteConfig.NumConn = 1 remoteConfig.NumConn = 1
seshMaker = func() *mux.Session { seshMaker = func() *client.CloakClient {
return client.MakeSession(remoteConfig, authInfo, d) return client.NewCloakClient(remoteConfig, authInfo, d)
} }
} else { } else {
var network string var network string
@ -173,7 +173,7 @@ func main() {
network = "TCP" network = "TCP"
} }
log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod) log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod)
seshMaker = func() *mux.Session { seshMaker = func() *client.CloakClient {
authInfo := authInfo // copy the struct because we are overwriting SessionId authInfo := authInfo // copy the struct because we are overwriting SessionId
randByte := make([]byte, 1) randByte := make([]byte, 1)
@ -185,7 +185,7 @@ func main() {
quad := make([]byte, 4) quad := make([]byte, 4)
common.RandRead(authInfo.WorldState.Rand, quad) common.RandRead(authInfo.WorldState.Rand, quad)
authInfo.SessionId = binary.BigEndian.Uint32(quad) authInfo.SessionId = binary.BigEndian.Uint32(quad)
return client.MakeSession(remoteConfig, authInfo, d) return client.NewCloakClient(remoteConfig, authInfo, d)
} }
} }
@ -195,12 +195,12 @@ func main() {
return net.ListenUDP("udp", udpAddr) return net.ListenUDP("udp", udpAddr)
} }
client.RouteUDP(acceptor, localConfig.Timeout, remoteConfig.Singleplex, seshMaker) cli_client.RouteUDP(acceptor, localConfig.Timeout, localConfig.Singleplex, seshMaker)
} else { } else {
listener, err := net.Listen("tcp", localConfig.LocalAddr) listener, err := net.Listen("tcp", localConfig.LocalAddr)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
client.RouteTCP(listener, localConfig.Timeout, remoteConfig.Singleplex, seshMaker) cli_client.RouteTCP(listener, localConfig.Timeout, localConfig.Singleplex, seshMaker)
} }
} }

@ -1,5 +1,4 @@
coverage: coverage:
status: status:
project: project: off
default: patch: off
threshold: 1%

@ -1,17 +1,30 @@
module github.com/cbeuw/Cloak module github.com/cbeuw/Cloak
go 1.14 go 1.21
toolchain go1.22.2
require ( require (
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.5.1
github.com/juju/ratelimit v1.0.1 github.com/juju/ratelimit v1.0.2
github.com/kr/pretty v0.1.0 // indirect github.com/refraction-networking/utls v1.6.4
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.9.0
gitlab.com/yawning/utls.git v0.0.12-1 go.etcd.io/bbolt v1.3.9
go.etcd.io/bbolt v1.3.6 golang.org/x/crypto v0.22.0
golang.org/x/crypto v0.1.0 )
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.19.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

104
go.sum

@ -1,76 +1,52 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs= github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs=
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE= github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/refraction-networking/utls v1.6.4 h1:aeynTroaYn7y+mFtqv8D0bQ4bw0y9nJHneGxJ7lvRDM=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/refraction-networking/utls v1.6.4/go.mod h1:2VL2xfiqgFAZtJKeUTlf+PSYFs3Eu7km0gCtXJ3m8zs=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
gitlab.com/yawning/utls.git v0.0.12-1 h1:RL6O0MP2YI0KghuEU/uGN6+8b4183eqNWoYgx7CXD0U= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
gitlab.com/yawning/utls.git v0.0.12-1/go.mod h1:3ONKiSFR9Im/c3t5RKmMJTVdmZN496FNyk3mjrY1dyo= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,154 @@
package cli_client
import (
"encoding/json"
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client"
log "github.com/sirupsen/logrus"
"io/ioutil"
"net"
"strings"
"time"
)
type CLIConfig struct {
client.Config
// LocalHost is the hostname or IP address to listen for incoming proxy client connections
LocalHost string // jsonOptional
// LocalPort is the port to listen for incomig proxy client connections
LocalPort string // jsonOptional
// AlternativeNames is a list of ServerName Cloak may randomly pick from for different sessions
// Optional
AlternativeNames []string
// StreamTimeout is the duration, in seconds, for an incoming connection to be automatically closed after the last
// piece of incoming data .
// Optional, Defaults to 300
StreamTimeout int
}
// 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", "UDP"}
lines := strings.Split(unescape(ssv), ";")
ret = []byte("{")
for _, ln := range lines {
if ln == "" {
break
}
sp := strings.SplitN(ln, "=", 2)
if len(sp) < 2 {
log.Errorf("Malformed config option: %v", ln)
continue
}
key := sp[0]
value := sp[1]
if strings.HasPrefix(key, "AlternativeNames") {
switch strings.Contains(value, ",") {
case true:
domains := strings.Split(value, ",")
for index, domain := range domains {
domains[index] = `"` + domain + `"`
}
value = strings.Join(domains, ",")
ret = append(ret, []byte(`"`+key+`":[`+value+`],`)...)
case false:
ret = append(ret, []byte(`"`+key+`":["`+value+`"],`)...)
}
continue
}
// JSON doesn't like quotation marks around int and bool
// This is extremely ugly but it's still better than writing a tokeniser
if elem(key, unquoted) {
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
}
func ParseConfig(conf string) (raw *CLIConfig, err error) {
var content []byte
// Checking if it's a path to json or a ssv string
if strings.Contains(conf, ";") && strings.Contains(conf, "=") {
content = ssvToJson(conf)
} else {
content, err = ioutil.ReadFile(conf)
if err != nil {
return
}
}
raw = new(CLIConfig)
err = json.Unmarshal(content, &raw)
if err != nil {
return
}
return
}
type LocalConnConfig struct {
LocalAddr string
Timeout time.Duration
MockDomainList []string
Singleplex bool
}
func (raw *CLIConfig) ProcessCLIConfig(worldState common.WorldState) (local LocalConnConfig, remote client.RemoteConnConfig, auth client.AuthInfo, err error) {
remote, auth, err = raw.Config.Process(worldState)
if err != nil {
return
}
if raw.AlternativeNames != nil && len(raw.AlternativeNames) > 0 {
var filteredAlternativeNames []string
for _, alternativeName := range raw.AlternativeNames {
if len(alternativeName) > 0 {
filteredAlternativeNames = append(filteredAlternativeNames, alternativeName)
}
}
local.MockDomainList = raw.AlternativeNames
} else {
local.MockDomainList = []string{}
}
local.MockDomainList = append(local.MockDomainList, auth.MockDomain)
if raw.LocalHost == "" {
err = fmt.Errorf("LocalHost cannot be empty")
return
}
if raw.LocalPort == "" {
err = fmt.Errorf("LocalPort cannot be empty")
return
}
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
}
local.Singleplex = raw.NumConn != nil && *raw.NumConn == 0
return
}

@ -0,0 +1,75 @@
package cli_client
import (
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client"
"github.com/stretchr/testify/assert"
"io/ioutil"
"testing"
)
func TestParseConfig(t *testing.T) {
ssv := "UID=iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=;PublicKey=IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=;" +
"ServerName=www.bing.com;NumConn=4;MaskBrowser=chrome;ProxyMethod=shadowsocks;EncryptionMethod=plain"
json := ssvToJson(ssv)
expected := []byte(`{"UID":"iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=","PublicKey":"IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=","ServerName":"www.bing.com","NumConn":4,"MaskBrowser":"chrome","ProxyMethod":"shadowsocks","EncryptionMethod":"plain"}`)
t.Run("byte equality", func(t *testing.T) {
assert.Equal(t, expected, json)
})
t.Run("struct equality", func(t *testing.T) {
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
_, _ = tmpConfig.Write(expected)
parsedFromSSV, err := ParseConfig(ssv)
assert.NoError(t, err)
parsedFromJson, err := ParseConfig(tmpConfig.Name())
assert.NoError(t, err)
assert.Equal(t, parsedFromJson, parsedFromSSV)
})
t.Run("empty file", func(t *testing.T) {
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
_, err := ParseConfig(tmpConfig.Name())
assert.Error(t, err)
})
}
func TestProcessCLIConfig(t *testing.T) {
config := CLIConfig{
Config: client.Config{
ServerName: "bbc.co.uk",
// ProxyMethod is the name of the underlying proxy you wish
// to connect to, as determined by your server. The value can
// be any string whose UTF-8 ENCODED byte length is no greater than
// 12 bytes
ProxyMethod: "ssh",
// UID is a 16-byte secret string unique to an authorised user
// The same UID can be used by the same user for multiple Cloak connections
UID: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
// PublicKey is the 32-byte public Curve25519 ECDH key of your server
PublicKey: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
// RemoteHost is the Cloak server's hostname or IP address
RemoteHost: "1.2.3.4",
},
LocalHost: "0.0.0.0",
LocalPort: "1234",
}
t.Run("Zero means singleplex", func(t *testing.T) {
zero := 0
config := config
config.NumConn = &zero
local, _, _, err := config.ProcessCLIConfig(common.RealWorldState)
assert.NoError(t, err)
assert.True(t, local.Singleplex)
})
t.Run("Empty means no singleplex", func(t *testing.T) {
config := config
local, _, _, err := config.ProcessCLIConfig(common.RealWorldState)
assert.NoError(t, err)
assert.False(t, local.Singleplex)
})
}

@ -1,25 +1,24 @@
package client package cli_client
import ( import (
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client"
"io" "io"
"net" "net"
"sync" "sync"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func RouteUDP(bindFunc func() (*net.UDPConn, error), streamTimeout time.Duration, singleplex bool, newSeshFunc func() *mux.Session) { func RouteUDP(bindFunc func() (*net.UDPConn, error), streamTimeout time.Duration, singleplex bool, newSeshFunc func() *client.CloakClient) {
var sesh *mux.Session var cloakClient *client.CloakClient
localConn, err := bindFunc() localConn, err := bindFunc()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
streams := make(map[string]*mux.Stream) streams := make(map[string]net.Conn)
var streamsMutex sync.Mutex var streamsMutex sync.Mutex
data := make([]byte, 8192) data := make([]byte, 8192)
@ -30,21 +29,21 @@ func RouteUDP(bindFunc func() (*net.UDPConn, error), streamTimeout time.Duration
continue continue
} }
if !singleplex && (sesh == nil || sesh.IsClosed()) { if !singleplex && (cloakClient == nil || cloakClient.IsClosed()) {
sesh = newSeshFunc() cloakClient = newSeshFunc()
} }
streamsMutex.Lock() streamsMutex.Lock()
stream, ok := streams[addr.String()] stream, ok := streams[addr.String()]
if !ok { if !ok {
if singleplex { if singleplex {
sesh = newSeshFunc() cloakClient = newSeshFunc()
} }
stream, err = sesh.OpenStream() stream, err = cloakClient.Dial()
if err != nil { if err != nil {
if singleplex { if singleplex {
sesh.Close() cloakClient.Close()
} }
log.Errorf("Failed to open stream: %v", err) log.Errorf("Failed to open stream: %v", err)
streamsMutex.Unlock() streamsMutex.Unlock()
@ -56,7 +55,7 @@ func RouteUDP(bindFunc func() (*net.UDPConn, error), streamTimeout time.Duration
_ = stream.SetReadDeadline(time.Now().Add(streamTimeout)) _ = stream.SetReadDeadline(time.Now().Add(streamTimeout))
proxyAddr := addr proxyAddr := addr
go func(stream *mux.Stream, localConn *net.UDPConn) { go func(stream net.Conn, localConn *net.UDPConn) {
buf := make([]byte, 8192) buf := make([]byte, 8192)
for { for {
n, err := stream.Read(buf) n, err := stream.Read(buf)
@ -95,18 +94,18 @@ func RouteUDP(bindFunc func() (*net.UDPConn, error), streamTimeout time.Duration
} }
} }
func RouteTCP(listener net.Listener, streamTimeout time.Duration, singleplex bool, newSeshFunc func() *mux.Session) { func RouteTCP(listener net.Listener, streamTimeout time.Duration, singleplex bool, newSeshFunc func() *client.CloakClient) {
var sesh *mux.Session var cloakClient *client.CloakClient
for { for {
localConn, err := listener.Accept() localConn, err := listener.Accept()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
continue continue
} }
if !singleplex && (sesh == nil || sesh.IsClosed()) { if !singleplex && (cloakClient == nil || cloakClient.IsClosed()) {
sesh = newSeshFunc() cloakClient = newSeshFunc()
} }
go func(sesh *mux.Session, localConn net.Conn, timeout time.Duration) { go func(sesh *client.CloakClient, localConn net.Conn, timeout time.Duration) {
if singleplex { if singleplex {
sesh = newSeshFunc() sesh = newSeshFunc()
} }
@ -122,7 +121,7 @@ func RouteTCP(listener net.Listener, streamTimeout time.Duration, singleplex boo
var zeroTime time.Time var zeroTime time.Time
_ = localConn.SetReadDeadline(zeroTime) _ = localConn.SetReadDeadline(zeroTime)
stream, err := sesh.OpenStream() stream, err := sesh.Dial()
if err != nil { if err != nil {
log.Errorf("Failed to open stream: %v", err) log.Errorf("Failed to open stream: %v", err)
localConn.Close() localConn.Close()
@ -148,6 +147,6 @@ func RouteTCP(listener net.Listener, streamTimeout time.Duration, singleplex boo
if _, err = common.Copy(stream, localConn); err != nil { if _, err = common.Copy(stream, localConn); err != nil {
log.Tracef("copying proxy client to stream: %v", err) log.Tracef("copying proxy client to stream: %v", err)
} }
}(sesh, localConn, streamTimeout) }(cloakClient, localConn, streamTimeout)
} }
} }

@ -1,110 +0,0 @@
package client
import (
"encoding/binary"
"encoding/hex"
"net"
"github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus"
)
const appDataMaxLength = 16401
type clientHelloFields struct {
random []byte
sessionId []byte
x25519KeyShare []byte
serverName string
}
func decodeHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
}
type browser interface {
composeClientHello(clientHelloFields) []byte
}
func generateSNI(serverName string) []byte {
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
}
// 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
}
type DirectTLS struct {
*common.TLSConn
browser browser
}
// NewClientTransport handles the TLS handshake for a given conn and returns the sessionKey
// if the server proceed with Cloak authentication
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
payload, sharedSecret := makeAuthenticationPayload(authInfo)
// random is marshalled ephemeral pub key 32 bytes
// The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare
fields := clientHelloFields{
random: payload.randPubKey[:],
sessionId: payload.ciphertextWithTag[0:32],
x25519KeyShare: payload.ciphertextWithTag[32:64],
serverName: authInfo.MockDomain,
}
chOnly := tls.browser.composeClientHello(fields)
chWithRecordLayer := common.AddRecordLayer(chOnly, common.Handshake, common.VersionTLS11)
_, err = rawConn.Write(chWithRecordLayer)
if err != nil {
return
}
log.Trace("client hello sent successfully")
tls.TLSConn = common.NewTLSConn(rawConn)
buf := make([]byte, 1024)
log.Trace("waiting for ServerHello")
_, err = tls.Read(buf)
if err != nil {
return
}
encrypted := append(buf[6:38], buf[84:116]...)
nonce := encrypted[0:12]
ciphertextWithTag := encrypted[12:60]
sessionKeySlice, err := common.AESGCMDecrypt(nonce, sharedSecret[:], ciphertextWithTag)
if err != nil {
return
}
copy(sessionKey[:], sessionKeySlice)
for i := 0; i < 2; i++ {
// ChangeCipherSpec and EncryptedCert (in the format of application data)
_, err = tls.Read(buf)
if err != nil {
return
}
}
return sessionKey, nil
}

@ -1,39 +0,0 @@
package client
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
)
func htob(s string) []byte {
b, _ := hex.DecodeString(s)
return b
}
func TestMakeServerName(t *testing.T) {
type testingPair struct {
serverName string
target []byte
}
pairs := []testingPair{
{
"www.google.com",
htob("001100000e7777772e676f6f676c652e636f6d"),
},
{
"www.gstatic.com",
htob("001200000f7777772e677374617469632e636f6d"),
},
{
"googleads.g.doubleclick.net",
htob("001e00001b676f6f676c656164732e672e646f75626c65636c69636b2e6e6574"),
},
}
for _, p := range pairs {
assert.Equal(t, p.target, generateSNI(p.serverName))
}
}

@ -1,120 +0,0 @@
// Fingerprint of Chrome 112
package client
import (
"encoding/binary"
"github.com/cbeuw/Cloak/internal/common"
"math/rand"
)
type Chrome struct{}
func makeGREASE() []byte {
// see https://tools.ietf.org/html/draft-davidben-tls-grease-01
// This is exclusive to Chrome.
var one [1]byte
common.CryptoRandRead(one[:])
sixteenth := one[0] % 16
monoGREASE := sixteenth*16 + 0xA
doubleGREASE := []byte{monoGREASE, monoGREASE}
return doubleGREASE
}
func (c *Chrome) composeExtensions(serverName string, keyShare []byte) []byte {
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
}
makeKeyShare := func(hidden []byte) []byte {
ret := make([]byte, 43)
ret[0], ret[1] = 0x00, 0x29 // length 41
copy(ret[2:4], makeGREASE())
ret[4], ret[5] = 0x00, 0x01 // length 1
ret[6] = 0x00
ret[7], ret[8] = 0x00, 0x1d // group x25519
ret[9], ret[10] = 0x00, 0x20 // length 32
copy(ret[11:43], hidden)
return ret
}
shuffle := func(exts [][]byte) {
var qword [8]byte
common.CryptoRandRead(qword[:])
seed := int64(binary.BigEndian.Uint64(qword[:]))
source := rand.NewSource(seed)
r := rand.New(source)
r.Shuffle(len(exts), func(i, j int) { exts[i], exts[j] = exts[j], exts[i] })
}
// extension length is always 403, and server name length is variable
var ext [18][]byte
ext[0] = addExtRec(makeGREASE(), nil) // First GREASE
// Start shufflable extensions: https://chromestatus.com/feature/5124606246518784
ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
sniLen := len(ext[1])
ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups
ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
ext[6] = addExtRec([]byte{0x00, 0x23}, nil) // Session tickets
ext[7] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
ext[8] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
ext[9] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001004030804040105030805050108060601")) // Signature Algorithms
ext[10] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp
ext[11] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share
ext[12] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
suppVersions := decodeHex("069A9A03040303") // 9A9A needs to be a GREASE
copy(suppVersions[1:3], makeGREASE())
ext[13] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
ext[14] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x02}) // compress certificate
ext[15] = addExtRec([]byte{0x44, 0x69}, decodeHex("0003026832")) // application settings
// End shufflable extensions
shuffle(ext[1:16])
ext[16] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE
// sniLen + len(all other ext) + len(ext[17]) = 403
// len(all other ext) = 175
// len(ext[17]) = 228 - sniLen
// 2+2+len(padding) = 228 - sniLen
// len(padding) = 224 - sniLen
ext[17] = addExtRec([]byte{0x00, 0x15}, make([]byte, 224-sniLen)) // padding
var ret []byte
for _, e := range ext {
ret = append(ret, e...)
}
return ret
}
func (c *Chrome) composeClientHello(hd clientHelloFields) (ch []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] = hd.random // random
clientHello[4] = []byte{0x20} // session id length 32
clientHello[5] = hd.sessionId // session id
clientHello[6] = []byte{0x00, 0x20} // cipher suites length 32
clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035")...) // cipher suites
clientHello[8] = []byte{0x01} // compression methods length 1
clientHello[9] = []byte{0x00} // compression methods
extensions := c.composeExtensions(hd.serverName, hd.x25519KeyShare)
clientHello[10] = []byte{0x00, 0x00}
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
clientHello[11] = extensions
var ret []byte
for _, c := range clientHello {
ret = append(ret, c...)
}
return ret
}

@ -1,60 +0,0 @@
package client
import (
"encoding/hex"
"testing"
)
func TestMakeGREASE(t *testing.T) {
a := hex.EncodeToString(makeGREASE())
if a[1] != 'a' || a[3] != 'a' {
t.Errorf("GREASE got %v", a)
}
var GREASEs []string
for i := 0; i < 50; i++ {
GREASEs = append(GREASEs, hex.EncodeToString(makeGREASE()))
}
var eqCount int
for _, g := range GREASEs {
if a == g {
eqCount++
}
}
if eqCount > 40 {
t.Error("GREASE is not random", GREASEs)
}
}
//func TestChromeJA3(t *testing.T) {
// result := common.AddRecordLayer((&Chrome{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
// assert.Equal(t, 517, len(result))
//
// hello := tlsx.ClientHelloBasic{}
// err := hello.Unmarshal(result)
// assert.Nil(t, err)
//
// // Chrome shuffles the order of extensions, so it needs special handling
// full := string(ja3.Bare(&hello))
// // TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
// parts := strings.Split(full, ",")
//
// // TLSVersion,Ciphers
// assert.Equal(t,
// []string{
// "771",
// "4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53",
// }, parts[0:2])
// // EllipticCurves,EllipticCurvePointFormats
// assert.Equal(t,
// []string{
// "29-23-24", "0",
// }, parts[3:5])
//
// normaliseExtensions := func(extensions string) []string {
// extensionParts := strings.Split(parts[2], "-")
// sort.Strings(extensionParts)
// return extensionParts
// }
// assert.Equal(t, normaliseExtensions("10-5-45-0-17513-13-18-11-23-16-35-27-65281-43-51-21"), normaliseExtensions(parts[2]))
//}

@ -1,73 +0,0 @@
// Fingerprint of Firefox 112
package client
import (
"encoding/binary"
"github.com/cbeuw/Cloak/internal/common"
)
type Firefox struct{}
func (f *Firefox) composeExtensions(serverName string, keyShare []byte) []byte {
composeKeyShare := func(hidden []byte) []byte {
ret := make([]byte, 107)
ret[0], ret[1] = 0x00, 0x69 // length 105
ret[2], ret[3] = 0x00, 0x1d // group x25519
ret[4], ret[5] = 0x00, 0x20 // length 32
copy(ret[6:38], hidden)
ret[38], ret[39] = 0x00, 0x17 // group secp256r1
ret[40], ret[41] = 0x00, 0x41 // length 65
common.CryptoRandRead(ret[42:107])
return ret
}
// extension length is always 401, and server name length is variable
var ext [13][]byte
ext[0] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
ext[1] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
ext[2] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
ext[3] = addExtRec([]byte{0x00, 0x0a}, decodeHex("000c001d00170018001901000101")) // supported groups
ext[4] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
ext[5] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
ext[6] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
ext[7] = addExtRec([]byte{0x00, 0x22}, decodeHex("00080403050306030203")) // delegated credentials
ext[8] = addExtRec([]byte{0x00, 0x33}, composeKeyShare(keyShare)) // key share
ext[9] = addExtRec([]byte{0x00, 0x2b}, decodeHex("0403040303")) // supported versions
ext[10] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030503060308040805080604010501060102030201")) // Signature Algorithms
ext[11] = addExtRec([]byte{0x00, 0x1c}, []byte{0x40, 0x01}) // record size limit
// len(ext[0]) + len(all other ext) + len(len field of padding) + len(padding) = 401
// len(all other ext) = 228
// len(len field of padding) = 4
// len(padding) = 169 - len(ext[0])
ext[12] = addExtRec([]byte{0x00, 0x15}, make([]byte, 169-len(ext[0]))) // padding
var ret []byte
for _, e := range ext {
ret = append(ret, e...)
}
return ret
}
func (f *Firefox) composeClientHello(hd clientHelloFields) (ch []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] = hd.random // random
clientHello[4] = []byte{0x20} // session id length 32
clientHello[5] = hd.sessionId // session id
clientHello[6] = []byte{0x00, 0x22} // cipher suites length 34
clientHello[7] = decodeHex("130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035") // cipher suites
clientHello[8] = []byte{0x01} // compression methods length 1
clientHello[9] = []byte{0x00} // compression methods
extensions := f.composeExtensions(hd.serverName, hd.x25519KeyShare)
clientHello[10] = []byte{0x00, 0x00}
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
clientHello[11] = extensions
var ret []byte
for _, c := range clientHello {
ret = append(ret, c...)
}
return ret
}

@ -1,40 +0,0 @@
package client
import (
"encoding/hex"
"github.com/stretchr/testify/assert"
"strings"
"testing"
)
var hd = clientHelloFields{
random: decodeHex("ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a0"),
sessionId: decodeHex("47485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b9"),
x25519KeyShare: decodeHex("d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa44"),
serverName: "github.com",
}
//func TestFirefoxJA3(t *testing.T) {
// result := common.AddRecordLayer((&Firefox{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
//
// hello := tlsx.ClientHelloBasic{}
// err := hello.Unmarshal(result)
// assert.Nil(t, err)
//
// digest := ja3.DigestHex(&hello)
// assert.Equal(t, "ad55557b7cbd735c2627f7ebb3b3d493", digest)
//}
func TestFirefoxComposeClientHello(t *testing.T) {
result := hex.EncodeToString((&Firefox{}).composeClientHello(hd))
target := "010001fc0303ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a02047485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b90022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035010001910000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b000201000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d0020d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa440017004104c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47002b00050403040303000d0018001604030503060308040805080604010501060102030201001c0002400100150096000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
// skip random secp256r1
secp256r1 := "04c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47"
start := strings.Index(target, secp256r1)
target = strings.Replace(target, secp256r1, "", 1)
result = strings.Replace(result, result[start:start+len(secp256r1)], "", 1)
assert.Equal(t, target, result)
}

@ -1,88 +0,0 @@
// Fingerprint of Safari 16.4
package client
import (
"encoding/binary"
)
type Safari struct{}
func (s *Safari) composeExtensions(serverName string, keyShare []byte) []byte {
makeSupportedGroups := func() []byte {
suppGroupListLen := []byte{0x00, 0x0a}
ret := make([]byte, 2+2+8)
copy(ret[0:2], suppGroupListLen)
copy(ret[2:4], makeGREASE())
copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19})
return ret
}
makeKeyShare := func(hidden []byte) []byte {
ret := make([]byte, 43)
ret[0], ret[1] = 0x00, 0x29 // length 41
copy(ret[2:4], makeGREASE())
ret[4], ret[5] = 0x00, 0x01 // length 1
ret[6] = 0x00
ret[7], ret[8] = 0x00, 0x1d // group x25519
ret[9], ret[10] = 0x00, 0x20 // length 32
copy(ret[11:43], hidden)
return ret
}
// extension length is always 393, and server name length is variable
var ext [16][]byte
ext[0] = addExtRec(makeGREASE(), nil) // First GREASE
ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups
ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
ext[6] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
ext[8] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030804040105030203080508050501080606010201")) // Signature Algorithms
ext[9] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp
ext[10] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share
ext[11] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
suppVersions := decodeHex("0a5a5a0304030303020301") // 5a5a needs to be a GREASE
copy(suppVersions[1:3], makeGREASE())
ext[12] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
ext[13] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x01}) // compress certificate
ext[14] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE
// len(ext[1]) + len(all other ext) + len(ext[15]) = 393
// len(all other ext) = 174
// len(ext[15]) = 219 - len(ext[1])
// 2+2+len(padding) = 219 - len(ext[1])
// len(padding) = 215 - len(ext[1])
ext[15] = addExtRec([]byte{0x00, 0x15}, make([]byte, 215-len(ext[1]))) // padding
var ret []byte
for _, e := range ext {
ret = append(ret, e...)
}
return ret
}
func (s *Safari) composeClientHello(hd clientHelloFields) (ch []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] = hd.random // random
clientHello[4] = []byte{0x20} // session id length 32
clientHello[5] = hd.sessionId // session id
clientHello[6] = []byte{0x00, 0x2a} // cipher suites length 42
clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a")...) // cipher suites
clientHello[8] = []byte{0x01} // compression methods length 1
clientHello[9] = []byte{0x00} // compression methods
extensions := s.composeExtensions(hd.serverName, hd.x25519KeyShare)
clientHello[10] = []byte{0x00, 0x00}
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
clientHello[11] = extensions
var ret []byte
for _, c := range clientHello {
ret = append(ret, c...)
}
return ret
}

@ -1,38 +0,0 @@
package client
import (
"testing"
)
var safariHd = clientHelloFields{
random: decodeHex("977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc17"),
sessionId: decodeHex("c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3"),
x25519KeyShare: decodeHex("c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a"),
serverName: "github.com",
}
//func TestSafariJA3(t *testing.T) {
// result := common.AddRecordLayer((&Safari{}).composeClientHello(safariHd), common.Handshake, common.VersionTLS11)
//
// hello := tlsx.ClientHelloBasic{}
// err := hello.Unmarshal(result)
// assert.Nil(t, err)
//
// digest := ja3.DigestHex(&hello)
// assert.Equal(t, "773906b0efdefa24a7f2b8eb6985bf37", digest)
//}
func TestSafariComposeClientHello(t *testing.T) {
result := (&Safari{}).composeClientHello(safariHd)
target := decodeHex("010001fc0303977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc1720c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3002acaca130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000189fafa00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000c000a7a7a001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d0018001604030804040105030203080508050501080606010201001200000033002b00297a7a000100001d0020c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a002d00020101002b000b0a2a2a0304030303020301001b00030200017a7a000100001500c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
for p := 0; p < len(result); p++ {
if result[p] != target[p] {
if result[p]&0x0F == 0xA && target[p]&0x0F == 0xA &&
((p > 0 && result[p-1] == result[p] && target[p-1] == target[p]) ||
(p < len(result)-1 && result[p+1] == result[p] && target[p+1] == target[p])) {
continue
}
t.Errorf("inequality at %v", p)
}
}
}

@ -1,281 +0,0 @@
package client
import (
"crypto"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"strings"
"time"
"github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus"
"github.com/cbeuw/Cloak/internal/ecdh"
mux "github.com/cbeuw/Cloak/internal/multiplex"
)
// RawConfig represents the fields in the config json file
// nullable means if it's empty, a default value will be chosen in ProcessRawConfig
// 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 ProcessRawConfig is called
type RawConfig struct {
ServerName string
ProxyMethod string
EncryptionMethod string
UID []byte
PublicKey []byte
NumConn int
LocalHost string // jsonOptional
LocalPort string // jsonOptional
RemoteHost string // jsonOptional
RemotePort string // jsonOptional
AlternativeNames []string // jsonOptional
// defaults set in ProcessRawConfig
UDP bool // nullable
BrowserSig string // nullable
Transport string // nullable
CDNOriginHost string // nullable
CDNWsUrlPath string // nullable
StreamTimeout int // nullable
KeepAlive int // nullable
}
type RemoteConnConfig struct {
Singleplex bool
NumConn int
KeepAlive time.Duration
RemoteAddr string
TransportMaker func() Transport
}
type LocalConnConfig struct {
LocalAddr string
Timeout time.Duration
MockDomainList []string
}
type AuthInfo struct {
UID []byte
SessionId uint32
ProxyMethod string
EncryptionMethod byte
Unordered bool
ServerPubKey crypto.PublicKey
MockDomain string
WorldState common.WorldState
}
// 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", "UDP"}
lines := strings.Split(unescape(ssv), ";")
ret = []byte("{")
for _, ln := range lines {
if ln == "" {
break
}
sp := strings.SplitN(ln, "=", 2)
if len(sp) < 2 {
log.Errorf("Malformed config option: %v", ln)
continue
}
key := sp[0]
value := sp[1]
if strings.HasPrefix(key, "AlternativeNames") {
switch strings.Contains(value, ",") {
case true:
domains := strings.Split(value, ",")
for index, domain := range domains {
domains[index] = `"` + domain + `"`
}
value = strings.Join(domains, ",")
ret = append(ret, []byte(`"`+key+`":[`+value+`],`)...)
case false:
ret = append(ret, []byte(`"`+key+`":["`+value+`"],`)...)
}
continue
}
// JSON doesn't like quotation marks around int and bool
// This is extremely ugly but it's still better than writing a tokeniser
if elem(key, unquoted) {
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
}
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, "=") {
content = ssvToJson(conf)
} else {
content, err = ioutil.ReadFile(conf)
if err != nil {
return
}
}
raw = new(RawConfig)
err = json.Unmarshal(content, &raw)
if err != nil {
return
}
return
}
func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (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.UID = raw.UID
auth.Unordered = raw.UDP
if raw.ServerName == "" {
return nullErr("ServerName")
}
auth.MockDomain = raw.ServerName
var filteredAlternativeNames []string
for _, alternativeName := range raw.AlternativeNames {
if len(alternativeName) > 0 {
filteredAlternativeNames = append(filteredAlternativeNames, alternativeName)
}
}
raw.AlternativeNames = filteredAlternativeNames
local.MockDomainList = raw.AlternativeNames
local.MockDomainList = append(local.MockDomainList, auth.MockDomain)
if raw.ProxyMethod == "" {
return nullErr("ServerName")
}
auth.ProxyMethod = raw.ProxyMethod
if len(raw.UID) == 0 {
return nullErr("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
auth.WorldState = worldState
// Encryption method
switch strings.ToLower(raw.EncryptionMethod) {
case "plain":
auth.EncryptionMethod = mux.EncryptionMethodPlain
case "aes-gcm", "aes-256-gcm":
auth.EncryptionMethod = mux.EncryptionMethodAES256GCM
case "aes-128-gcm":
auth.EncryptionMethod = mux.EncryptionMethodAES128GCM
case "chacha20-poly1305":
auth.EncryptionMethod = mux.EncryptionMethodChaha20Poly1305
default:
err = fmt.Errorf("unknown encryption method %v", raw.EncryptionMethod)
return
}
if raw.RemoteHost == "" {
return nullErr("RemoteHost")
}
if raw.RemotePort == "" {
return nullErr("RemotePort")
}
remote.RemoteAddr = net.JoinHostPort(raw.RemoteHost, raw.RemotePort)
if raw.NumConn <= 0 {
remote.NumConn = 1
remote.Singleplex = true
} else {
remote.NumConn = raw.NumConn
remote.Singleplex = false
}
// Transport and (if TLS mode), browser
switch strings.ToLower(raw.Transport) {
case "cdn":
var cdnDomainPort string
if raw.CDNOriginHost == "" {
cdnDomainPort = net.JoinHostPort(raw.RemoteHost, raw.RemotePort)
} else {
cdnDomainPort = net.JoinHostPort(raw.CDNOriginHost, raw.RemotePort)
}
if raw.CDNWsUrlPath == "" {
raw.CDNWsUrlPath = "/"
}
remote.TransportMaker = func() Transport {
return &WSOverTLS{
wsUrl: "ws://" + cdnDomainPort + raw.CDNWsUrlPath,
}
}
case "direct":
fallthrough
default:
var browser browser
switch strings.ToLower(raw.BrowserSig) {
case "firefox":
browser = &Firefox{}
case "safari":
browser = &Safari{}
case "chrome":
fallthrough
default:
browser = &Chrome{}
}
remote.TransportMaker = func() Transport {
return &DirectTLS{
browser: browser,
}
}
}
// KeepAlive
if raw.KeepAlive <= 0 {
remote.KeepAlive = -1
} else {
remote.KeepAlive = remote.KeepAlive * time.Second
}
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
}

@ -1,37 +0,0 @@
package client
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseConfig(t *testing.T) {
ssv := "UID=iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=;PublicKey=IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=;" +
"ServerName=www.bing.com;NumConn=4;MaskBrowser=chrome;ProxyMethod=shadowsocks;EncryptionMethod=plain"
json := ssvToJson(ssv)
expected := []byte(`{"UID":"iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=","PublicKey":"IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=","ServerName":"www.bing.com","NumConn":4,"MaskBrowser":"chrome","ProxyMethod":"shadowsocks","EncryptionMethod":"plain"}`)
t.Run("byte equality", func(t *testing.T) {
assert.Equal(t, expected, json)
})
t.Run("struct equality", func(t *testing.T) {
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
_, _ = tmpConfig.Write(expected)
parsedFromSSV, err := ParseConfig(ssv)
assert.NoError(t, err)
parsedFromJson, err := ParseConfig(tmpConfig.Name())
assert.NoError(t, err)
assert.Equal(t, parsedFromJson, parsedFromSSV)
})
t.Run("empty file", func(t *testing.T) {
tmpConfig, _ := ioutil.TempFile("", "ck_client_config")
_, err := ParseConfig(tmpConfig.Name())
assert.Error(t, err)
})
}

@ -1,10 +0,0 @@
package client
import (
"net"
)
type Transport interface {
Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error)
net.Conn
}

@ -66,46 +66,6 @@ func (d *datagramBufferedPipe) Read(target []byte) (int, error) {
return dataLen, nil return dataLen, nil
} }
func (d *datagramBufferedPipe) WriteTo(w io.Writer) (n int64, err error) {
d.rwCond.L.Lock()
defer d.rwCond.L.Unlock()
for {
if d.closed && len(d.pLens) == 0 {
return 0, io.EOF
}
hasRDeadline := !d.rDeadline.IsZero()
if hasRDeadline {
if time.Until(d.rDeadline) <= 0 {
return 0, ErrTimeout
}
}
if len(d.pLens) > 0 {
var dataLen int
dataLen, d.pLens = d.pLens[0], d.pLens[1:]
written, er := w.Write(d.buf.Next(dataLen))
n += int64(written)
if er != nil {
d.rwCond.Broadcast()
return n, er
}
d.rwCond.Broadcast()
} else {
if d.wtTimeout == 0 {
if hasRDeadline {
d.broadcastAfter(time.Until(d.rDeadline))
}
} else {
d.rDeadline = time.Now().Add(d.wtTimeout)
d.broadcastAfter(d.wtTimeout)
}
d.rwCond.Wait()
}
}
}
func (d *datagramBufferedPipe) Write(f *Frame) (toBeClosed bool, err error) { func (d *datagramBufferedPipe) Write(f *Frame) (toBeClosed bool, err error) {
d.rwCond.L.Lock() d.rwCond.L.Lock()
defer d.rwCond.L.Unlock() defer d.rwCond.L.Unlock()
@ -151,14 +111,6 @@ func (d *datagramBufferedPipe) SetReadDeadline(t time.Time) {
d.rwCond.Broadcast() d.rwCond.Broadcast()
} }
func (d *datagramBufferedPipe) SetWriteToTimeout(t time.Duration) {
d.rwCond.L.Lock()
defer d.rwCond.L.Unlock()
d.wtTimeout = t
d.rwCond.Broadcast()
}
func (d *datagramBufferedPipe) broadcastAfter(t time.Duration) { func (d *datagramBufferedPipe) broadcastAfter(t time.Duration) {
if d.timeoutTimer != nil { if d.timeoutTimer != nil {
d.timeoutTimer.Stop() d.timeoutTimer.Stop()

@ -14,12 +14,8 @@ type recvBuffer interface {
// Instead, it should behave as if it hasn't been closed. Closure is only relevant // Instead, it should behave as if it hasn't been closed. Closure is only relevant
// when the buffer is empty. // when the buffer is empty.
io.ReadCloser io.ReadCloser
io.WriterTo
Write(*Frame) (toBeClosed bool, err error) Write(*Frame) (toBeClosed bool, err error)
SetReadDeadline(time time.Time) SetReadDeadline(time time.Time)
// SetWriteToTimeout sets the duration a recvBuffer waits in a WriteTo call when nothing
// has been written for a while. After that duration it should return ErrTimeout
SetWriteToTimeout(d time.Duration)
} }
// size we want the amount of unread data in buffer to grow before recvBuffer.Write blocks. // size we want the amount of unread data in buffer to grow before recvBuffer.Write blocks.

@ -14,15 +14,13 @@ import (
) )
const ( const (
acceptBacklog = 1024 acceptBacklog = 1024
defaultInactivityTimeout = 30 * time.Second defaultMaxOnWireSize = 1<<14 + 256 // https://tools.ietf.org/html/rfc8446#section-5.2
defaultMaxOnWireSize = 1<<14 + 256 // https://tools.ietf.org/html/rfc8446#section-5.2
) )
var ErrBrokenSession = errors.New("broken session") var ErrBrokenSession = errors.New("broken session")
var errRepeatSessionClosing = errors.New("trying to close a closed session") var errRepeatSessionClosing = errors.New("trying to close a closed session")
var errRepeatStreamClosing = errors.New("trying to close a closed stream") var errRepeatStreamClosing = errors.New("trying to close a closed stream")
var errNoMultiplex = errors.New("a singleplexing session can have only one stream")
type SessionConfig struct { type SessionConfig struct {
Obfuscator Obfuscator
@ -30,15 +28,15 @@ type SessionConfig struct {
// Valve is used to limit transmission rates, and record and limit usage // Valve is used to limit transmission rates, and record and limit usage
Valve Valve
// Unordered determines whether stream packets' order is preserved
Unordered bool Unordered bool
// A Singleplexing session always has just one stream // MsgOnWireSizeLimit is maximum size of an obfuscated frame, including headers and overhead
Singleplex bool // Optional
// maximum size of an obfuscated frame, including headers and overhead
MsgOnWireSizeLimit int MsgOnWireSizeLimit int
// InactivityTimeout sets the duration a Session waits while it has no active streams before it closes itself // InactivityTimeout sets the duration a Session waits while it has no active streams before it closes itself
// Non-optional. 0 means the session closes immediately after the last stream is closed
InactivityTimeout time.Duration InactivityTimeout time.Duration
} }
@ -104,9 +102,6 @@ func MakeSession(id uint32, config SessionConfig) *Session {
if config.MsgOnWireSizeLimit <= 0 { if config.MsgOnWireSizeLimit <= 0 {
sesh.MsgOnWireSizeLimit = defaultMaxOnWireSize sesh.MsgOnWireSizeLimit = defaultMaxOnWireSize
} }
if config.InactivityTimeout == 0 {
sesh.InactivityTimeout = defaultInactivityTimeout
}
sesh.maxStreamUnitWrite = sesh.MsgOnWireSizeLimit - frameHeaderLength - sesh.maxOverhead sesh.maxStreamUnitWrite = sesh.MsgOnWireSizeLimit - frameHeaderLength - sesh.maxOverhead
sesh.streamSendBufferSize = sesh.MsgOnWireSizeLimit sesh.streamSendBufferSize = sesh.MsgOnWireSizeLimit
@ -118,7 +113,13 @@ func MakeSession(id uint32, config SessionConfig) *Session {
}} }}
sesh.sb = makeSwitchboard(sesh) sesh.sb = makeSwitchboard(sesh)
time.AfterFunc(sesh.InactivityTimeout, sesh.checkTimeout) if sesh.InactivityTimeout > 0 {
time.AfterFunc(sesh.InactivityTimeout, sesh.checkTimeout)
} else {
// The user wants session to close immediately after the last stream closes,
// but we still give them some time to start a stream first
time.AfterFunc(10*time.Second, sesh.checkTimeout)
}
return sesh return sesh
} }
@ -149,12 +150,6 @@ func (sesh *Session) OpenStream() (*Stream, error) {
return nil, ErrBrokenSession return nil, ErrBrokenSession
} }
id := atomic.AddUint32(&sesh.nextStreamID, 1) - 1 id := atomic.AddUint32(&sesh.nextStreamID, 1) - 1
// Because atomic.AddUint32 returns the value after incrementation
if sesh.Singleplex && id > 1 {
// if there are more than one streams, which shouldn't happen if we are
// singleplexing
return nil, errNoMultiplex
}
stream := makeStream(sesh, id) stream := makeStream(sesh, id)
sesh.streamsM.Lock() sesh.streamsM.Lock()
sesh.streams[id] = stream sesh.streams[id] = stream
@ -213,12 +208,8 @@ func (sesh *Session) closeStream(s *Stream, active bool) error {
sesh.streams[s.id] = nil sesh.streams[s.id] = nil
sesh.streamsM.Unlock() sesh.streamsM.Unlock()
if sesh.streamCountDecr() == 0 { if sesh.streamCountDecr() == 0 {
if sesh.Singleplex { log.Debugf("session %v has no active stream left", sesh.id)
return sesh.Close() time.AfterFunc(sesh.InactivityTimeout, sesh.checkTimeout)
} else {
log.Debugf("session %v has no active stream left", sesh.id)
time.AfterFunc(sesh.InactivityTimeout, sesh.checkTimeout)
}
} }
return nil return nil
} }

@ -17,8 +17,8 @@ import (
) )
var seshConfigs = map[string]SessionConfig{ var seshConfigs = map[string]SessionConfig{
"ordered": {}, "ordered": {InactivityTimeout: 30 * time.Second},
"unordered": {Unordered: true}, "unordered": {Unordered: true, InactivityTimeout: 30 * time.Second},
} }
var encryptionMethods = map[string]byte{ var encryptionMethods = map[string]byte{
"plain": EncryptionMethodPlain, "plain": EncryptionMethodPlain,
@ -557,7 +557,7 @@ func BenchmarkRecvDataFromRemote(b *testing.B) {
go func() { go func() {
stream, _ := sesh.Accept() stream, _ := sesh.Accept()
stream.(*Stream).WriteTo(ioutil.Discard) io.Copy(ioutil.Discard, stream)
}() }()
binaryFrames := [maxIter][]byte{} binaryFrames := [maxIter][]byte{}

@ -96,17 +96,6 @@ func (s *Stream) Read(buf []byte) (n int, err error) {
return return
} }
// WriteTo continuously write data Stream has received into the writer w.
func (s *Stream) WriteTo(w io.Writer) (int64, error) {
// will keep writing until the underlying buffer is closed
n, err := s.recvBuf.WriteTo(w)
log.Tracef("%v read from stream %v with err %v", n, s.id, err)
if err == io.EOF {
return n, ErrBrokenStream
}
return n, nil
}
func (s *Stream) obfuscateAndSend(buf []byte, payloadOffsetInBuf int) error { func (s *Stream) obfuscateAndSend(buf []byte, payloadOffsetInBuf int) error {
cipherTextLen, err := s.session.obfuscate(&s.writingFrame, buf, payloadOffsetInBuf) cipherTextLen, err := s.session.obfuscate(&s.writingFrame, buf, payloadOffsetInBuf)
s.writingFrame.Seq++ s.writingFrame.Seq++
@ -210,7 +199,6 @@ func (s *Stream) Close() error {
func (s *Stream) LocalAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[0] } func (s *Stream) LocalAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[0] }
func (s *Stream) RemoteAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[1] } func (s *Stream) RemoteAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[1] }
func (s *Stream) SetWriteToTimeout(d time.Duration) { s.recvBuf.SetWriteToTimeout(d) }
func (s *Stream) SetReadDeadline(t time.Time) error { s.recvBuf.SetReadDeadline(t); return nil } func (s *Stream) SetReadDeadline(t time.Time) error { s.recvBuf.SetReadDeadline(t); return nil }
func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d } func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d }

@ -13,7 +13,6 @@ package multiplex
import ( import (
"container/heap" "container/heap"
"fmt" "fmt"
"io"
"sync" "sync"
"time" "time"
) )
@ -102,10 +101,6 @@ func (sb *streamBuffer) Read(buf []byte) (int, error) {
return sb.buf.Read(buf) return sb.buf.Read(buf)
} }
func (sb *streamBuffer) WriteTo(w io.Writer) (int64, error) {
return sb.buf.WriteTo(w)
}
func (sb *streamBuffer) Close() error { func (sb *streamBuffer) Close() error {
sb.recvM.Lock() sb.recvM.Lock()
defer sb.recvM.Unlock() defer sb.recvM.Unlock()
@ -113,5 +108,4 @@ func (sb *streamBuffer) Close() error {
return sb.buf.Close() return sb.buf.Close()
} }
func (sb *streamBuffer) SetReadDeadline(t time.Time) { sb.buf.SetReadDeadline(t) } func (sb *streamBuffer) SetReadDeadline(t time.Time) { sb.buf.SetReadDeadline(t) }
func (sb *streamBuffer) SetWriteToTimeout(d time.Duration) { sb.buf.SetWriteToTimeout(d) }

@ -58,43 +58,6 @@ func (p *streamBufferedPipe) Read(target []byte) (int, error) {
return n, err return n, err
} }
func (p *streamBufferedPipe) WriteTo(w io.Writer) (n int64, err error) {
p.rwCond.L.Lock()
defer p.rwCond.L.Unlock()
for {
if p.closed && p.buf.Len() == 0 {
return 0, io.EOF
}
hasRDeadline := !p.rDeadline.IsZero()
if hasRDeadline {
if time.Until(p.rDeadline) <= 0 {
return 0, ErrTimeout
}
}
if p.buf.Len() > 0 {
written, er := p.buf.WriteTo(w)
n += written
if er != nil {
p.rwCond.Broadcast()
return n, er
}
p.rwCond.Broadcast()
} else {
if p.wtTimeout == 0 {
if hasRDeadline {
p.broadcastAfter(time.Until(p.rDeadline))
}
} else {
p.rDeadline = time.Now().Add(p.wtTimeout)
p.broadcastAfter(p.wtTimeout)
}
p.rwCond.Wait()
}
}
}
func (p *streamBufferedPipe) Write(input []byte) (int, error) { func (p *streamBufferedPipe) Write(input []byte) (int, error) {
p.rwCond.L.Lock() p.rwCond.L.Lock()
defer p.rwCond.L.Unlock() defer p.rwCond.L.Unlock()
@ -131,14 +94,6 @@ func (p *streamBufferedPipe) SetReadDeadline(t time.Time) {
p.rwCond.Broadcast() p.rwCond.Broadcast()
} }
func (p *streamBufferedPipe) SetWriteToTimeout(d time.Duration) {
p.rwCond.L.Lock()
defer p.rwCond.L.Unlock()
p.wtTimeout = d
p.rwCond.Broadcast()
}
func (p *streamBufferedPipe) broadcastAfter(d time.Duration) { func (p *streamBufferedPipe) broadcastAfter(d time.Duration) {
if p.timeoutTimer != nil { if p.timeoutTimer != nil {
p.timeoutTimer.Stop() p.timeoutTimer.Stop()

@ -3,7 +3,6 @@ package multiplex
import ( import (
"bytes" "bytes"
"io" "io"
"io/ioutil"
"math/rand" "math/rand"
"testing" "testing"
"time" "time"
@ -81,19 +80,21 @@ func TestStream_WriteSync(t *testing.T) {
// Close calls made after write MUST have a higher seq // Close calls made after write MUST have a higher seq
var sessionKey [32]byte var sessionKey [32]byte
rand.Read(sessionKey[:]) rand.Read(sessionKey[:])
clientSesh := setupSesh(false, sessionKey, EncryptionMethodPlain)
serverSesh := setupSesh(false, sessionKey, EncryptionMethodPlain)
w, r := connutil.AsyncPipe()
clientSesh.AddConnection(common.NewTLSConn(w))
serverSesh.AddConnection(common.NewTLSConn(r))
testData := make([]byte, payloadLen) testData := make([]byte, payloadLen)
rand.Read(testData) rand.Read(testData)
t.Run("test single", func(t *testing.T) { t.Run("test single", func(t *testing.T) {
clientSesh := setupSesh(false, sessionKey, EncryptionMethodPlain)
serverSesh := setupSesh(false, sessionKey, EncryptionMethodPlain)
w, r := connutil.AsyncPipe()
clientSesh.AddConnection(common.NewTLSConn(w))
serverSesh.AddConnection(common.NewTLSConn(r))
go func() { go func() {
stream, _ := clientSesh.OpenStream() stream, err := clientSesh.OpenStream()
stream.Write(testData) assert.NoError(t, err)
stream.Close() _, err = stream.Write(testData)
assert.NoError(t, err)
}() }()
recvBuf := make([]byte, payloadLen) recvBuf := make([]byte, payloadLen)
@ -105,12 +106,19 @@ func TestStream_WriteSync(t *testing.T) {
}) })
t.Run("test multiple", func(t *testing.T) { t.Run("test multiple", func(t *testing.T) {
clientSesh := setupSesh(false, sessionKey, EncryptionMethodPlain)
serverSesh := setupSesh(false, sessionKey, EncryptionMethodPlain)
w, r := connutil.AsyncPipe()
clientSesh.AddConnection(common.NewTLSConn(w))
serverSesh.AddConnection(common.NewTLSConn(r))
const numStreams = 100 const numStreams = 100
for i := 0; i < numStreams; i++ { for i := 0; i < numStreams; i++ {
go func() { go func() {
stream, _ := clientSesh.OpenStream() stream, err := clientSesh.OpenStream()
stream.Write(testData) assert.NoError(t, err)
stream.Close() _, err = stream.Write(testData)
assert.NoError(t, err)
}() }()
} }
for i := 0; i < numStreams; i++ { for i := 0; i < numStreams; i++ {
@ -364,31 +372,6 @@ func TestStream_Read(t *testing.T) {
} }
} }
func TestStream_SetWriteToTimeout(t *testing.T) {
seshes := map[string]*Session{
"ordered": setupSesh(false, emptyKey, EncryptionMethodPlain),
"unordered": setupSesh(true, emptyKey, EncryptionMethodPlain),
}
for name, sesh := range seshes {
t.Run(name, func(t *testing.T) {
stream, _ := sesh.OpenStream()
stream.SetWriteToTimeout(100 * time.Millisecond)
done := make(chan struct{})
go func() {
stream.WriteTo(ioutil.Discard)
done <- struct{}{}
}()
select {
case <-done:
return
case <-time.After(500 * time.Millisecond):
t.Error("didn't timeout")
}
})
}
}
func TestStream_SetReadFromTimeout(t *testing.T) { func TestStream_SetReadFromTimeout(t *testing.T) {
seshes := map[string]*Session{ seshes := map[string]*Session{
"ordered": setupSesh(false, emptyKey, EncryptionMethodPlain), "ordered": setupSesh(false, emptyKey, EncryptionMethodPlain),

@ -164,12 +164,12 @@ func parseClientHello(data []byte) (ret *ClientHello, err error) {
func composeServerHello(sessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte) []byte { func composeServerHello(sessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte) []byte {
var serverHello [11][]byte var serverHello [11][]byte
serverHello[0] = []byte{0x02} // handshake type serverHello[0] = []byte{0x02} // handshake type
serverHello[1] = []byte{0x00, 0x00, 0x76} // length 77 serverHello[1] = []byte{0x00, 0x00, 0x76} // length 118
serverHello[2] = []byte{0x03, 0x03} // server version serverHello[2] = []byte{0x03, 0x03} // server version
serverHello[3] = append(nonce[0:12], encryptedSessionKeyWithTag[0:20]...) // random 32 bytes serverHello[3] = append(nonce[0:12], encryptedSessionKeyWithTag[0:20]...) // random 32 bytes
serverHello[4] = []byte{0x20} // session id length 32 serverHello[4] = []byte{0x20} // session id length 32
serverHello[5] = sessionId // session id serverHello[5] = sessionId // session id
serverHello[6] = []byte{0xc0, 0x30} // cipher suite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 serverHello[6] = []byte{0x13, 0x02} // cipher suite TLS_AES_256_GCM_SHA384
serverHello[7] = []byte{0x00} // compression method null serverHello[7] = []byte{0x00} // compression method null
serverHello[8] = []byte{0x00, 0x2e} // extensions length 46 serverHello[8] = []byte{0x00, 0x2e} // extensions length 46

@ -5,6 +5,13 @@ import (
"encoding/base64" "encoding/base64"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/cli_client"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/server"
"github.com/cbeuw/Cloak/libcloak/client"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
"io" "io"
"math/rand" "math/rand"
"net" "net"
@ -12,13 +19,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/cbeuw/Cloak/internal/client"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/server"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -76,60 +76,56 @@ func serveUDPEcho(listener *connutil.PipeListener) {
var bypassUID = [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} var bypassUID = [16]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
var publicKey, _ = base64.StdEncoding.DecodeString("7f7TuKrs264VNSgMno8PkDlyhGhVuOSR8JHLE6H4Ljc=") var publicKey, _ = base64.StdEncoding.DecodeString("7f7TuKrs264VNSgMno8PkDlyhGhVuOSR8JHLE6H4Ljc=")
var privateKey, _ = base64.StdEncoding.DecodeString("SMWeC6VuZF8S/id65VuFQFlfa7hTEJBpL6wWhqPP100=") var privateKey, _ = base64.StdEncoding.DecodeString("SMWeC6VuZF8S/id65VuFQFlfa7hTEJBpL6wWhqPP100=")
var four = 4
var zero = 0
var basicUDPConfig = client.RawConfig{ var basicUDPConfig = client.Config{
ServerName: "www.example.com", ServerName: "www.example.com",
ProxyMethod: "openvpn", ProxyMethod: "openvpn",
EncryptionMethod: "plain", EncryptionMethod: "plain",
UID: bypassUID[:], UID: bypassUID[:],
PublicKey: publicKey, PublicKey: publicKey,
NumConn: 4, NumConn: &four,
UDP: true, UDP: true,
Transport: "direct", Transport: "direct",
RemoteHost: "fake.com", RemoteHost: "fake.com",
RemotePort: "9999", RemotePort: "9999",
LocalHost: "127.0.0.1",
LocalPort: "9999",
} }
var basicTCPConfig = client.RawConfig{ var basicTCPConfig = client.Config{
ServerName: "www.example.com", ServerName: "www.example.com",
ProxyMethod: "shadowsocks", ProxyMethod: "shadowsocks",
EncryptionMethod: "plain", EncryptionMethod: "plain",
UID: bypassUID[:], UID: bypassUID[:],
PublicKey: publicKey, PublicKey: publicKey,
NumConn: 4, NumConn: &four,
UDP: false, UDP: false,
Transport: "direct", Transport: "direct",
RemoteHost: "fake.com", RemoteHost: "fake.com",
RemotePort: "9999", RemotePort: "9999",
LocalHost: "127.0.0.1",
LocalPort: "9999",
BrowserSig: "firefox", BrowserSig: "firefox",
} }
var singleplexTCPConfig = client.RawConfig{ var singleplexTCPConfig = client.Config{
ServerName: "www.example.com", ServerName: "www.example.com",
ProxyMethod: "shadowsocks", ProxyMethod: "shadowsocks",
EncryptionMethod: "plain", EncryptionMethod: "plain",
UID: bypassUID[:], UID: bypassUID[:],
PublicKey: publicKey, PublicKey: publicKey,
NumConn: 0, NumConn: &zero,
UDP: false, UDP: false,
Transport: "direct", Transport: "direct",
RemoteHost: "fake.com", RemoteHost: "fake.com",
RemotePort: "9999", RemotePort: "9999",
LocalHost: "127.0.0.1", BrowserSig: "chrome",
LocalPort: "9999",
BrowserSig: "safari",
} }
func generateClientConfigs(rawConfig client.RawConfig, state common.WorldState) (client.LocalConnConfig, client.RemoteConnConfig, client.AuthInfo) { func generateClientConfigs(rawConfig client.Config, state common.WorldState) (client.RemoteConnConfig, client.AuthInfo) {
lcl, rmt, auth, err := rawConfig.ProcessRawConfig(state) rmt, auth, err := rawConfig.Process(state)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return lcl, rmt, auth return rmt, auth
} }
func basicServerState(ws common.WorldState) *server.State { func basicServerState(ws common.WorldState) *server.State {
@ -161,7 +157,7 @@ func (m *mockUDPDialer) Dial(network, address string) (net.Conn, error) {
return net.DialUDP("udp", nil, m.raddr) return net.DialUDP("udp", nil, m.raddr)
} }
func establishSession(lcc client.LocalConnConfig, rcc client.RemoteConnConfig, ai client.AuthInfo, serverState *server.State) (common.Dialer, *connutil.PipeListener, common.Dialer, net.Listener, error) { func establishSession(rcc client.RemoteConnConfig, ai client.AuthInfo, serverState *server.State, singleplex bool) (common.Dialer, *connutil.PipeListener, common.Dialer, net.Listener, error) {
// redirecting web server // redirecting web server
// ^ // ^
// | // |
@ -178,14 +174,17 @@ func establishSession(lcc client.LocalConnConfig, rcc client.RemoteConnConfig, a
// | // |
// whatever connection initiator (including a proper ck-client) // whatever connection initiator (including a proper ck-client)
if singleplex && rcc.NumConn != 1 {
log.Fatal("NumConn must be 1 under singleplex")
}
netToCkServerD, ckServerListener := connutil.DialerListener(10 * 1024) netToCkServerD, ckServerListener := connutil.DialerListener(10 * 1024)
clientSeshMaker := func() *mux.Session { clientSeshMaker := func() *client.CloakClient {
ai := ai ai := ai
quad := make([]byte, 4) quad := make([]byte, 4)
common.RandRead(ai.WorldState.Rand, quad) common.RandRead(ai.WorldState.Rand, quad)
ai.SessionId = binary.BigEndian.Uint32(quad) ai.SessionId = binary.BigEndian.Uint32(quad)
return client.MakeSession(rcc, ai, netToCkServerD) return client.NewCloakClient(rcc, ai, netToCkServerD)
} }
var proxyToCkClientD common.Dialer var proxyToCkClientD common.Dialer
@ -202,12 +201,12 @@ func establishSession(lcc client.LocalConnConfig, rcc client.RemoteConnConfig, a
addrCh <- conn.LocalAddr().(*net.UDPAddr) addrCh <- conn.LocalAddr().(*net.UDPAddr)
return conn, err return conn, err
} }
go client.RouteUDP(acceptor, lcc.Timeout, rcc.Singleplex, clientSeshMaker) go cli_client.RouteUDP(acceptor, 300*time.Second, singleplex, clientSeshMaker)
proxyToCkClientD = mDialer proxyToCkClientD = mDialer
} else { } else {
var proxyToCkClientL *connutil.PipeListener var proxyToCkClientL *connutil.PipeListener
proxyToCkClientD, proxyToCkClientL = connutil.DialerListener(10 * 1024) proxyToCkClientD, proxyToCkClientL = connutil.DialerListener(10 * 1024)
go client.RouteTCP(proxyToCkClientL, lcc.Timeout, rcc.Singleplex, clientSeshMaker) go cli_client.RouteTCP(proxyToCkClientL, 300*time.Second, singleplex, clientSeshMaker)
} }
// set up server // set up server
@ -259,15 +258,15 @@ func TestUDP(t *testing.T) {
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0)) worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(basicUDPConfig, worldState) rcc, ai := generateClientConfigs(basicUDPConfig, worldState)
sta := basicServerState(worldState) sta := basicServerState(worldState)
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(lcc, rcc, ai, sta)
if err != nil {
t.Fatal(err)
}
t.Run("simple send", func(t *testing.T) { t.Run("simple send", func(t *testing.T) {
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, false)
if err != nil {
t.Fatal(err)
}
pxyClientConn, err := proxyToCkClientD.Dial("udp", "") pxyClientConn, err := proxyToCkClientD.Dial("udp", "")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -300,6 +299,11 @@ func TestUDP(t *testing.T) {
const echoMsgLen = 1024 const echoMsgLen = 1024
t.Run("user echo", func(t *testing.T) { t.Run("user echo", func(t *testing.T) {
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, false)
if err != nil {
t.Fatal(err)
}
go serveUDPEcho(proxyFromCkServerL) go serveUDPEcho(proxyFromCkServerL)
var conn [1]net.Conn var conn [1]net.Conn
conn[0], err = proxyToCkClientD.Dial("udp", "") conn[0], err = proxyToCkClientD.Dial("udp", "")
@ -315,9 +319,9 @@ func TestUDP(t *testing.T) {
func TestTCPSingleplex(t *testing.T) { func TestTCPSingleplex(t *testing.T) {
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0)) worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(singleplexTCPConfig, worldState) rcc, ai := generateClientConfigs(singleplexTCPConfig, worldState)
sta := basicServerState(worldState) sta := basicServerState(worldState)
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(lcc, rcc, ai, sta) proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -376,20 +380,20 @@ func TestTCPMultiplex(t *testing.T) {
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0)) worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(basicTCPConfig, worldState) rcc, ai := generateClientConfigs(basicTCPConfig, worldState)
sta := basicServerState(worldState) sta := basicServerState(worldState)
proxyToCkClientD, proxyFromCkServerL, netToCkServerD, redirFromCkServerL, err := establishSession(lcc, rcc, ai, sta)
if err != nil {
t.Fatal(err)
}
t.Run("user echo single", func(t *testing.T) { t.Run("user echo single", func(t *testing.T) {
for i := 0; i < 18; i += 2 { for i := 0; i < 18; i += 2 {
dataLen := 1 << i dataLen := 1 << i
writeData := make([]byte, dataLen) writeData := make([]byte, dataLen)
rand.Read(writeData) rand.Read(writeData)
t.Run(fmt.Sprintf("data length %v", dataLen), func(t *testing.T) { t.Run(fmt.Sprintf("data length %v", dataLen), func(t *testing.T) {
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, false)
if err != nil {
t.Fatal(err)
}
go serveTCPEcho(proxyFromCkServerL) go serveTCPEcho(proxyFromCkServerL)
conn, err := proxyToCkClientD.Dial("", "") conn, err := proxyToCkClientD.Dial("", "")
if err != nil { if err != nil {
@ -418,6 +422,11 @@ func TestTCPMultiplex(t *testing.T) {
const echoMsgLen = 16384 const echoMsgLen = 16384
t.Run("user echo", func(t *testing.T) { t.Run("user echo", func(t *testing.T) {
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, false)
if err != nil {
t.Fatal(err)
}
go serveTCPEcho(proxyFromCkServerL) go serveTCPEcho(proxyFromCkServerL)
var conns [numConns]net.Conn var conns [numConns]net.Conn
for i := 0; i < numConns; i++ { for i := 0; i < numConns; i++ {
@ -431,6 +440,11 @@ func TestTCPMultiplex(t *testing.T) {
}) })
t.Run("redir echo", func(t *testing.T) { t.Run("redir echo", func(t *testing.T) {
_, _, netToCkServerD, redirFromCkServerL, err := establishSession(rcc, ai, sta, false)
if err != nil {
t.Fatal(err)
}
go serveTCPEcho(redirFromCkServerL) go serveTCPEcho(redirFromCkServerL)
var conns [numConns]net.Conn var conns [numConns]net.Conn
for i := 0; i < numConns; i++ { for i := 0; i < numConns; i++ {
@ -447,13 +461,13 @@ func TestClosingStreamsFromProxy(t *testing.T) {
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0)) worldState := common.WorldOfTime(time.Unix(10, 0))
for clientConfigName, clientConfig := range map[string]client.RawConfig{"basic": basicTCPConfig, "singleplex": singleplexTCPConfig} { for clientConfigName, clientConfig := range map[string]client.Config{"multiplex": basicTCPConfig, "singleplex": singleplexTCPConfig} {
clientConfig := clientConfig
clientConfigName := clientConfigName clientConfigName := clientConfigName
clientConfig := clientConfig
t.Run(clientConfigName, func(t *testing.T) { t.Run(clientConfigName, func(t *testing.T) {
lcc, rcc, ai := generateClientConfigs(clientConfig, worldState) rcc, ai := generateClientConfigs(clientConfig, worldState)
sta := basicServerState(worldState) sta := basicServerState(worldState)
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(lcc, rcc, ai, sta) proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, *clientConfig.NumConn == 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -513,7 +527,7 @@ func TestClosingStreamsFromProxy(t *testing.T) {
func BenchmarkIntegration(b *testing.B) { func BenchmarkIntegration(b *testing.B) {
log.SetLevel(log.ErrorLevel) log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0)) worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(basicTCPConfig, worldState) rcc, ai := generateClientConfigs(basicTCPConfig, worldState)
sta := basicServerState(worldState) sta := basicServerState(worldState)
const bufSize = 16 * 1024 const bufSize = 16 * 1024
@ -527,7 +541,7 @@ func BenchmarkIntegration(b *testing.B) {
for name, method := range encryptionMethods { for name, method := range encryptionMethods {
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
ai.EncryptionMethod = method ai.EncryptionMethod = method
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(lcc, rcc, ai, sta) proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, false)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }

@ -0,0 +1,206 @@
package client
import (
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client/transports"
"net"
"strings"
"time"
"github.com/cbeuw/Cloak/internal/ecdh"
mux "github.com/cbeuw/Cloak/internal/multiplex"
)
// Config contains the configuration parameter fields for a Cloak client
type Config struct {
// Required fields
// ServerName is the domain you appear to be visiting
// to your Firewall or ISP
ServerName string
// ProxyMethod is the name of the underlying proxy you wish
// to connect to, as determined by your server. The value can
// be any string whose UTF-8 ENCODED byte length is no greater than
// 12 bytes
ProxyMethod string
// UID is a 16-byte secret string unique to an authorised user
// The same UID can be used by the same user for multiple Cloak connections
UID []byte
// PublicKey is the 32-byte public Curve25519 ECDH key of your server
PublicKey []byte
// RemoteHost is the Cloak server's hostname or IP address
RemoteHost string
// Optional Fields
// EncryptionMethod is the cryptographic algorithm used to
// encrypt data on the wire.
// Valid values are `aes-128-gcm`, `aes-256-gcm`, `chacha20-poly1305`, and `plain`
// Defaults to `aes-256-gcm`
EncryptionMethod string
// NumConn is the amount of underlying TLS connections to establish with Cloak server.
// Cloak multiplexes any number of incoming connections to a fixed number of underlying TLS connections.
// If set to 0, a special singleplex mode is enabled: each incoming connection will correspond to exactly one
// TLS connection
// Defaults to 4
NumConn *int
// UDP enables UDP semantics, where packets must fit into one unit of message (below 16000 bytes by default),
// and packets can be received out of order. Though reliable delivery is still guaranteed.
UDP bool
// BrowserSig is the browser signature to be used. Options are `chrome` and `firefox`
// Defaults to `chrome`
BrowserSig string
// Transport is either `direct` or `cdn`. Under `direct`, the client connects to a Cloak server directly.
// Under `cdn`, the client connects to a CDN provider such as Amazon Cloudfront, which in turn connects
// to a Cloak server.
// Defaults to `direct`
Transport string
// CDNOriginHost is the CDN Origin's (i.e. Cloak server) real hostname or IP address, which is encrypted between
// the client and the CDN server, and therefore hidden to ISP or firewalls. This only has effect when Transport
// is set to `cdn`
// Defaults to RemoteHost
CDNOriginHost string
// KeepAlive is the interval between TCP KeepAlive packets to be sent over the underlying TLS connections
// Defaults to -1, which means no TCP KeepAlive is ever sent
KeepAlive int
// RemotePort is the port Cloak server is listening to
// Defaults to 443
RemotePort string
// InactivityTimeout is the number of seconds the client keeps the underlying connections to the server
// after the last proxy connection is disconnected.
// Defaults to 30. Always set to 0 under Singleplex mode (NumConn == 0)
InactivityTimeout *int
}
type RemoteConnConfig struct {
NumConn int
KeepAlive time.Duration
RemoteAddr string
TransportMaker func() transports.Transport
InactivityTimeout time.Duration
}
type AuthInfo = transports.AuthInfo
func (raw *Config) Process(worldState common.WorldState) (remote RemoteConnConfig, auth AuthInfo, err error) {
if raw.ServerName == "" {
err = fmt.Errorf("ServerName cannot be empty")
return
}
if raw.ProxyMethod == "" {
err = fmt.Errorf("ProxyMethod cannot be empty")
return
}
if len(raw.UID) == 0 {
err = fmt.Errorf("UID cannot be empty")
return
}
if len(raw.PublicKey) == 0 {
err = fmt.Errorf("PublicKey cannot be empty")
return
}
if raw.RemoteHost == "" {
err = fmt.Errorf("RemoteHost cannot be empty")
return
}
auth.UID = raw.UID
auth.Unordered = raw.UDP
auth.MockDomain = raw.ServerName
auth.ProxyMethod = raw.ProxyMethod
auth.WorldState = worldState
// static public key
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":
auth.EncryptionMethod = mux.EncryptionMethodPlain
case "aes-gcm", "aes-256-gcm", "":
auth.EncryptionMethod = mux.EncryptionMethodAES256GCM
case "aes-128-gcm":
auth.EncryptionMethod = mux.EncryptionMethodAES128GCM
case "chacha20-poly1305":
auth.EncryptionMethod = mux.EncryptionMethodChaha20Poly1305
default:
err = fmt.Errorf("unknown encryption method %v", raw.EncryptionMethod)
return
}
var remotePort string
if raw.RemotePort == "" {
remotePort = "443"
} else {
remotePort = raw.RemotePort
}
remote.RemoteAddr = net.JoinHostPort(raw.RemoteHost, remotePort)
if raw.InactivityTimeout == nil {
remote.InactivityTimeout = 30 * time.Second
} else {
remote.InactivityTimeout = time.Duration(*raw.InactivityTimeout) * time.Second
}
if raw.NumConn == nil {
remote.NumConn = 4
} else if *raw.NumConn <= 0 {
remote.NumConn = 1
remote.InactivityTimeout = 0
} else {
remote.NumConn = *raw.NumConn
}
// Transport and (if TLS mode), browser
switch strings.ToLower(raw.Transport) {
case "cdn":
cdnPort := raw.RemotePort
var cdnHost string
if raw.CDNOriginHost == "" {
cdnHost = raw.RemoteHost
} else {
cdnHost = raw.CDNOriginHost
}
remote.TransportMaker = func() transports.Transport {
return &transports.WSOverTLS{
CDNHost: cdnHost,
CDNPort: cdnPort,
}
}
case "direct", "":
var browser transports.Browser
switch strings.ToLower(raw.BrowserSig) {
case "firefox":
browser = transports.Firefox
case "safari":
browser = transports.Safari
case "chrome", "":
browser = transports.Chrome
default:
err = fmt.Errorf("unknown browser signature %v", raw.BrowserSig)
return
}
remote.TransportMaker = func() transports.Transport {
return &transports.DirectTLS{
Browser: browser,
}
}
default:
err = fmt.Errorf("unknown transport %v", raw.Transport)
return
}
// KeepAlive
if raw.KeepAlive <= 0 {
remote.KeepAlive = -1
} else {
remote.KeepAlive = remote.KeepAlive * time.Second
}
return
}

@ -0,0 +1,73 @@
package client
import (
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/stretchr/testify/assert"
"reflect"
"testing"
)
var baseConfig = Config{
ServerName: "www.bing.com",
ProxyMethod: "ssh",
UID: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf},
PublicKey: make([]byte, 32),
RemoteHost: "12.34.56.78",
}
func TestDefault(t *testing.T) {
remote, auth, err := baseConfig.Process(common.RealWorldState)
assert.NoError(t, err)
assert.EqualValues(t, 4, remote.NumConn)
assert.EqualValues(t, mux.EncryptionMethodAES256GCM, auth.EncryptionMethod)
assert.EqualValues(t, -1, remote.KeepAlive)
assert.False(t, auth.Unordered)
}
func TestValidation(t *testing.T) {
_, _, err := baseConfig.Process(common.RealWorldState)
assert.NoError(t, err)
type test struct {
fieldToChange string
newValue any
errPattern string
}
tests := []test{
{
fieldToChange: "ServerName",
newValue: "",
errPattern: "empty",
},
{
fieldToChange: "UID",
newValue: []byte{},
errPattern: "empty",
},
{
fieldToChange: "PublicKey",
newValue: []byte{0x1},
errPattern: "unmarshal",
},
{
fieldToChange: "RemoteHost",
newValue: "",
errPattern: "empty",
},
{
fieldToChange: "BrowserSig",
newValue: "not-a-browser",
errPattern: "unknown",
},
}
for _, test := range tests {
config := baseConfig
reflect.ValueOf(&config).Elem().FieldByName(test.fieldToChange).Set(reflect.ValueOf(test.newValue))
_, _, err := config.Process(common.RealWorldState)
assert.ErrorContains(t, err, test.errPattern)
}
}

@ -12,8 +12,18 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// On different invocations to MakeSession, authInfo.SessionId MUST be different type CloakClient struct {
func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.Dialer) *mux.Session { connConfig RemoteConnConfig
authInfo AuthInfo
dialer common.Dialer
session *mux.Session
}
const appDataMaxLength = 16401
// On different invocations to NewCloakClient, authInfo.SessionId MUST be different
func NewCloakClient(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.Dialer) *CloakClient {
log.Info("Attempting to start a new session") log.Info("Attempting to start a new session")
connsCh := make(chan net.Conn, connConfig.NumConn) connsCh := make(chan net.Conn, connConfig.NumConn)
@ -34,8 +44,8 @@ func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.D
transportConn := connConfig.TransportMaker() transportConn := connConfig.TransportMaker()
sk, err := transportConn.Handshake(remoteConn, authInfo) sk, err := transportConn.Handshake(remoteConn, authInfo)
if err != nil { if err != nil {
transportConn.Close()
log.Errorf("Failed to prepare connection to remote: %v", err) log.Errorf("Failed to prepare connection to remote: %v", err)
transportConn.Close()
time.Sleep(time.Second * 3) time.Sleep(time.Second * 3)
goto makeconn goto makeconn
} }
@ -55,19 +65,37 @@ func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.D
} }
seshConfig := mux.SessionConfig{ seshConfig := mux.SessionConfig{
Singleplex: connConfig.Singleplex,
Obfuscator: obfuscator, Obfuscator: obfuscator,
Valve: nil, Valve: nil,
Unordered: authInfo.Unordered, Unordered: authInfo.Unordered,
MsgOnWireSizeLimit: appDataMaxLength, MsgOnWireSizeLimit: appDataMaxLength,
InactivityTimeout: connConfig.InactivityTimeout,
} }
sesh := mux.MakeSession(authInfo.SessionId, seshConfig) session := mux.MakeSession(authInfo.SessionId, seshConfig)
for i := 0; i < connConfig.NumConn; i++ { for i := 0; i < connConfig.NumConn; i++ {
conn := <-connsCh conn := <-connsCh
sesh.AddConnection(conn) session.AddConnection(conn)
} }
log.Infof("Session %v established", authInfo.SessionId) log.Infof("Session %v established", authInfo.SessionId)
return sesh
return &CloakClient{
connConfig: connConfig,
authInfo: authInfo,
dialer: dialer,
session: session,
}
}
func (client *CloakClient) Dial() (net.Conn, error) {
return client.session.OpenStream()
}
func (client *CloakClient) Close() error {
return client.session.Close()
}
func (client *CloakClient) IsClosed() bool {
return client.session.IsClosed()
} }

@ -0,0 +1,126 @@
package transports
import (
"github.com/cbeuw/Cloak/internal/common"
utls "github.com/refraction-networking/utls"
log "github.com/sirupsen/logrus"
"net"
)
type clientHelloFields struct {
random []byte
sessionId []byte
x25519KeyShare []byte
serverName string
}
type Browser int
const (
Chrome = iota
Firefox
Safari
)
type DirectTLS struct {
*common.TLSConn
Browser Browser
}
func buildClientHello(browser Browser, fields clientHelloFields) ([]byte, error) {
// We don't use utls to handle connections (as it'll attempt a real TLS negotiation)
// We only want it to build the ClientHello locally
fakeConn := net.TCPConn{}
var helloID utls.ClientHelloID
switch browser {
case Chrome:
helloID = utls.HelloChrome_Auto
case Firefox:
helloID = utls.HelloFirefox_Auto
case Safari:
helloID = utls.HelloSafari_Auto
}
uclient := utls.UClient(&fakeConn, &utls.Config{ServerName: fields.serverName}, helloID)
if err := uclient.BuildHandshakeState(); err != nil {
return []byte{}, err
}
if err := uclient.SetClientRandom(fields.random); err != nil {
return []byte{}, err
}
uclient.HandshakeState.Hello.SessionId = make([]byte, 32)
copy(uclient.HandshakeState.Hello.SessionId, fields.sessionId)
// Find the X25519 key share and overwrite it
var extIndex int
var keyShareIndex int
for i, ext := range uclient.Extensions {
ext, ok := ext.(*utls.KeyShareExtension)
if ok {
extIndex = i
for j, keyShare := range ext.KeyShares {
if keyShare.Group == utls.X25519 {
keyShareIndex = j
}
}
}
}
copy(uclient.Extensions[extIndex].(*utls.KeyShareExtension).KeyShares[keyShareIndex].Data, fields.x25519KeyShare)
if err := uclient.BuildHandshakeState(); err != nil {
return []byte{}, err
}
return uclient.HandshakeState.Hello.Raw, nil
}
// Handshake handles the TLS handshake for a given conn and returns the sessionKey
// if the server proceed with Cloak authentication
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
payload, sharedSecret := makeAuthenticationPayload(authInfo)
fields := clientHelloFields{
random: payload.randPubKey[:],
sessionId: payload.ciphertextWithTag[0:32],
x25519KeyShare: payload.ciphertextWithTag[32:64],
serverName: authInfo.MockDomain,
}
var ch []byte
ch, err = buildClientHello(tls.Browser, fields)
if err != nil {
return
}
chWithRecordLayer := common.AddRecordLayer(ch, common.Handshake, common.VersionTLS11)
_, err = rawConn.Write(chWithRecordLayer)
if err != nil {
return
}
log.Trace("client hello sent successfully")
tls.TLSConn = common.NewTLSConn(rawConn)
buf := make([]byte, 1024)
log.Trace("waiting for ServerHello")
_, err = tls.Read(buf)
if err != nil {
return
}
encrypted := append(buf[6:38], buf[84:116]...)
nonce := encrypted[0:12]
ciphertextWithTag := encrypted[12:60]
sessionKeySlice, err := common.AESGCMDecrypt(nonce, sharedSecret[:], ciphertextWithTag)
if err != nil {
return
}
copy(sessionKey[:], sessionKeySlice)
for i := 0; i < 2; i++ {
// ChangeCipherSpec and EncryptedCert (in the format of application data)
_, err = tls.Read(buf)
if err != nil {
return
}
}
return sessionKey, nil
}

@ -1,4 +1,4 @@
package client package transports
import ( import (
"encoding/binary" "encoding/binary"

@ -1,4 +1,4 @@
package client package transports
import ( import (
"bytes" "bytes"

@ -0,0 +1,23 @@
package transports
import (
"crypto"
"github.com/cbeuw/Cloak/internal/common"
"net"
)
type Transport interface {
Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error)
net.Conn
}
type AuthInfo struct {
UID []byte
SessionId uint32
ProxyMethod string
EncryptionMethod byte
Unordered bool
ServerPubKey crypto.PublicKey
MockDomain string
WorldState common.WorldState
}

@ -1,21 +1,21 @@
package client package transports
import ( import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/gorilla/websocket"
utls "github.com/refraction-networking/utls"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"github.com/cbeuw/Cloak/internal/common"
"github.com/gorilla/websocket"
utls "gitlab.com/yawning/utls.git"
) )
type WSOverTLS struct { type WSOverTLS struct {
*common.WebSocketConn *common.WebSocketConn
wsUrl string CDNHost string
CDNPort string
} }
func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) { func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
@ -41,7 +41,7 @@ func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey
return return
} }
u, err := url.Parse(ws.wsUrl) u, err := url.Parse("ws://" + net.JoinHostPort(ws.CDNHost, ws.CDNPort))
if err != nil { if err != nil {
return sessionKey, fmt.Errorf("failed to parse ws url: %v", err) return sessionKey, fmt.Errorf("failed to parse ws url: %v", err)
} }

@ -0,0 +1,13 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"packageRules": [
{
"packagePatterns": ["*"],
"excludePackagePatterns": ["utls"],
"enabled": false
}
]
}
Loading…
Cancel
Save