Compare commits

...

8 Commits

@ -123,7 +123,7 @@ upstream proxy server. Zero or negative value disables it. Default is 0 (disable
`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
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.
@ -131,7 +131,7 @@ instead a CDN is used, use `CDN`.
server's `ProxyBook` exactly.
`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
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).**
@ -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
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:
@ -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
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
behave like GoQuiet. This maybe useful for people with unstable connections.
TCP connection will spawn a separate short-lived session that will be closed after it is terminated. This maybe useful
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.
Currently, `chrome`, `firefox` and `safari` are supported.

@ -8,19 +8,22 @@ import (
"encoding/binary"
"flag"
"fmt"
"github.com/cbeuw/Cloak/internal/cli_client"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client"
"net"
"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"
)
var version string
func main() {
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
// Should be 127.0.0.1 to listen to a proxy client on this machine
var localHost string
// port used by proxy clients to communicate with cloak client
@ -75,16 +78,13 @@ func main() {
log.Info("Starting standalone mode")
}
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
lvl, err := log.ParseLevel(*verbosity)
if err != nil {
log.Fatal(err)
}
log.SetLevel(lvl)
rawConfig, err := client.ParseConfig(config)
rawConfig, err := cli_client.ParseConfig(config)
if err != nil {
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 {
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}
@ -162,8 +162,8 @@ func main() {
authInfo.SessionId = 0
remoteConfig.NumConn = 1
seshMaker = func() *mux.Session {
return client.MakeSession(remoteConfig, authInfo, d)
seshMaker = func() *client.CloakClient {
return client.NewCloakClient(remoteConfig, authInfo, d)
}
} else {
var network string
@ -173,7 +173,7 @@ func main() {
network = "TCP"
}
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
randByte := make([]byte, 1)
@ -185,7 +185,7 @@ func main() {
quad := make([]byte, 4)
common.RandRead(authInfo.WorldState.Rand, 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)
}
client.RouteUDP(acceptor, localConfig.Timeout, remoteConfig.Singleplex, seshMaker)
cli_client.RouteUDP(acceptor, localConfig.Timeout, localConfig.Singleplex, seshMaker)
} else {
listener, err := net.Listen("tcp", localConfig.LocalAddr)
if err != nil {
log.Fatal(err)
}
client.RouteTCP(listener, localConfig.Timeout, remoteConfig.Singleplex, seshMaker)
cli_client.RouteTCP(listener, localConfig.Timeout, localConfig.Singleplex, seshMaker)
}
}

@ -1,17 +1,26 @@
module github.com/cbeuw/Cloak
go 1.14
go 1.18
require (
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/juju/ratelimit v1.0.1
github.com/kr/pretty v0.1.0 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.6.1
github.com/stretchr/testify v1.7.1
gitlab.com/yawning/utls.git v0.0.12-1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.1.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
golang.org/x/sys v0.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

@ -25,10 +25,9 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
gitlab.com/yawning/utls.git v0.0.12-1 h1:RL6O0MP2YI0KghuEU/uGN6+8b4183eqNWoYgx7CXD0U=
@ -37,38 +36,16 @@ go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

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

@ -14,15 +14,13 @@ import (
)
const (
acceptBacklog = 1024
defaultInactivityTimeout = 30 * time.Second
defaultMaxOnWireSize = 1<<14 + 256 // https://tools.ietf.org/html/rfc8446#section-5.2
acceptBacklog = 1024
defaultMaxOnWireSize = 1<<14 + 256 // https://tools.ietf.org/html/rfc8446#section-5.2
)
var ErrBrokenSession = errors.New("broken session")
var errRepeatSessionClosing = errors.New("trying to close a closed session")
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 {
Obfuscator
@ -30,15 +28,15 @@ type SessionConfig struct {
// Valve is used to limit transmission rates, and record and limit usage
Valve
// Unordered determines whether stream packets' order is preserved
Unordered bool
// A Singleplexing session always has just one stream
Singleplex bool
// maximum size of an obfuscated frame, including headers and overhead
// MsgOnWireSizeLimit is maximum size of an obfuscated frame, including headers and overhead
// Optional
MsgOnWireSizeLimit int
// 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
}
@ -104,9 +102,6 @@ func MakeSession(id uint32, config SessionConfig) *Session {
if config.MsgOnWireSizeLimit <= 0 {
sesh.MsgOnWireSizeLimit = defaultMaxOnWireSize
}
if config.InactivityTimeout == 0 {
sesh.InactivityTimeout = defaultInactivityTimeout
}
sesh.maxStreamUnitWrite = sesh.MsgOnWireSizeLimit - frameHeaderLength - sesh.maxOverhead
sesh.streamSendBufferSize = sesh.MsgOnWireSizeLimit
@ -118,7 +113,13 @@ func MakeSession(id uint32, config SessionConfig) *Session {
}}
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
}
@ -149,12 +150,6 @@ func (sesh *Session) OpenStream() (*Stream, error) {
return nil, ErrBrokenSession
}
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)
sesh.streamsM.Lock()
sesh.streams[id] = stream
@ -213,12 +208,8 @@ func (sesh *Session) closeStream(s *Stream, active bool) error {
sesh.streams[s.id] = nil
sesh.streamsM.Unlock()
if sesh.streamCountDecr() == 0 {
if sesh.Singleplex {
return sesh.Close()
} else {
log.Debugf("session %v has no active stream left", sesh.id)
time.AfterFunc(sesh.InactivityTimeout, sesh.checkTimeout)
}
log.Debugf("session %v has no active stream left", sesh.id)
time.AfterFunc(sesh.InactivityTimeout, sesh.checkTimeout)
}
return nil
}

@ -17,8 +17,8 @@ import (
)
var seshConfigs = map[string]SessionConfig{
"ordered": {},
"unordered": {Unordered: true},
"ordered": {InactivityTimeout: 30 * time.Second},
"unordered": {Unordered: true, InactivityTimeout: 30 * time.Second},
}
var encryptionMethods = map[string]byte{
"plain": EncryptionMethodPlain,

@ -81,19 +81,21 @@ func TestStream_WriteSync(t *testing.T) {
// Close calls made after write MUST have a higher seq
var sessionKey [32]byte
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)
rand.Read(testData)
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() {
stream, _ := clientSesh.OpenStream()
stream.Write(testData)
stream.Close()
stream, err := clientSesh.OpenStream()
assert.NoError(t, err)
_, err = stream.Write(testData)
assert.NoError(t, err)
}()
recvBuf := make([]byte, payloadLen)
@ -105,12 +107,19 @@ func TestStream_WriteSync(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
for i := 0; i < numStreams; i++ {
go func() {
stream, _ := clientSesh.OpenStream()
stream.Write(testData)
stream.Close()
stream, err := clientSesh.OpenStream()
assert.NoError(t, err)
_, err = stream.Write(testData)
assert.NoError(t, err)
}()
}
for i := 0; i < numStreams; i++ {

@ -5,6 +5,13 @@ import (
"encoding/base64"
"encoding/binary"
"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"
"math/rand"
"net"
@ -12,13 +19,6 @@ import (
"testing"
"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"
)
@ -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 publicKey, _ = base64.StdEncoding.DecodeString("7f7TuKrs264VNSgMno8PkDlyhGhVuOSR8JHLE6H4Ljc=")
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",
ProxyMethod: "openvpn",
EncryptionMethod: "plain",
UID: bypassUID[:],
PublicKey: publicKey,
NumConn: 4,
NumConn: &four,
UDP: true,
Transport: "direct",
RemoteHost: "fake.com",
RemotePort: "9999",
LocalHost: "127.0.0.1",
LocalPort: "9999",
}
var basicTCPConfig = client.RawConfig{
var basicTCPConfig = client.Config{
ServerName: "www.example.com",
ProxyMethod: "shadowsocks",
EncryptionMethod: "plain",
UID: bypassUID[:],
PublicKey: publicKey,
NumConn: 4,
NumConn: &four,
UDP: false,
Transport: "direct",
RemoteHost: "fake.com",
RemotePort: "9999",
LocalHost: "127.0.0.1",
LocalPort: "9999",
BrowserSig: "firefox",
}
var singleplexTCPConfig = client.RawConfig{
var singleplexTCPConfig = client.Config{
ServerName: "www.example.com",
ProxyMethod: "shadowsocks",
EncryptionMethod: "plain",
UID: bypassUID[:],
PublicKey: publicKey,
NumConn: 0,
NumConn: &zero,
UDP: false,
Transport: "direct",
RemoteHost: "fake.com",
RemotePort: "9999",
LocalHost: "127.0.0.1",
LocalPort: "9999",
BrowserSig: "safari",
BrowserSig: "chrome",
}
func generateClientConfigs(rawConfig client.RawConfig, state common.WorldState) (client.LocalConnConfig, client.RemoteConnConfig, client.AuthInfo) {
lcl, rmt, auth, err := rawConfig.ProcessRawConfig(state)
func generateClientConfigs(rawConfig client.Config, state common.WorldState) (client.RemoteConnConfig, client.AuthInfo) {
rmt, auth, err := rawConfig.Process(state)
if err != nil {
log.Fatal(err)
}
return lcl, rmt, auth
return rmt, auth
}
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)
}
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
// ^
// |
@ -178,14 +174,17 @@ func establishSession(lcc client.LocalConnConfig, rcc client.RemoteConnConfig, a
// |
// 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)
clientSeshMaker := func() *mux.Session {
clientSeshMaker := func() *client.CloakClient {
ai := ai
quad := make([]byte, 4)
common.RandRead(ai.WorldState.Rand, quad)
ai.SessionId = binary.BigEndian.Uint32(quad)
return client.MakeSession(rcc, ai, netToCkServerD)
return client.NewCloakClient(rcc, ai, netToCkServerD)
}
var proxyToCkClientD common.Dialer
@ -202,12 +201,12 @@ func establishSession(lcc client.LocalConnConfig, rcc client.RemoteConnConfig, a
addrCh <- conn.LocalAddr().(*net.UDPAddr)
return conn, err
}
go client.RouteUDP(acceptor, lcc.Timeout, rcc.Singleplex, clientSeshMaker)
go cli_client.RouteUDP(acceptor, 300*time.Second, singleplex, clientSeshMaker)
proxyToCkClientD = mDialer
} else {
var proxyToCkClientL *connutil.PipeListener
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
@ -259,15 +258,15 @@ func TestUDP(t *testing.T) {
log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(basicUDPConfig, worldState)
rcc, ai := generateClientConfigs(basicUDPConfig, 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) {
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, false)
if err != nil {
t.Fatal(err)
}
pxyClientConn, err := proxyToCkClientD.Dial("udp", "")
if err != nil {
t.Error(err)
@ -300,6 +299,11 @@ func TestUDP(t *testing.T) {
const echoMsgLen = 1024
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)
var conn [1]net.Conn
conn[0], err = proxyToCkClientD.Dial("udp", "")
@ -315,9 +319,9 @@ func TestUDP(t *testing.T) {
func TestTCPSingleplex(t *testing.T) {
log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(singleplexTCPConfig, worldState)
rcc, ai := generateClientConfigs(singleplexTCPConfig, worldState)
sta := basicServerState(worldState)
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(lcc, rcc, ai, sta)
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, true)
if err != nil {
t.Fatal(err)
}
@ -376,20 +380,20 @@ func TestTCPMultiplex(t *testing.T) {
log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(basicTCPConfig, worldState)
rcc, ai := generateClientConfigs(basicTCPConfig, 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) {
for i := 0; i < 18; i += 2 {
dataLen := 1 << i
writeData := make([]byte, dataLen)
rand.Read(writeData)
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)
conn, err := proxyToCkClientD.Dial("", "")
if err != nil {
@ -418,6 +422,11 @@ func TestTCPMultiplex(t *testing.T) {
const echoMsgLen = 16384
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)
var conns [numConns]net.Conn
for i := 0; i < numConns; i++ {
@ -431,6 +440,11 @@ func TestTCPMultiplex(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)
var conns [numConns]net.Conn
for i := 0; i < numConns; i++ {
@ -447,13 +461,13 @@ func TestClosingStreamsFromProxy(t *testing.T) {
log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0))
for clientConfigName, clientConfig := range map[string]client.RawConfig{"basic": basicTCPConfig, "singleplex": singleplexTCPConfig} {
clientConfig := clientConfig
for clientConfigName, clientConfig := range map[string]client.Config{"multiplex": basicTCPConfig, "singleplex": singleplexTCPConfig} {
clientConfigName := clientConfigName
clientConfig := clientConfig
t.Run(clientConfigName, func(t *testing.T) {
lcc, rcc, ai := generateClientConfigs(clientConfig, worldState)
rcc, ai := generateClientConfigs(clientConfig, 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 {
t.Fatal(err)
}
@ -513,7 +527,7 @@ func TestClosingStreamsFromProxy(t *testing.T) {
func BenchmarkIntegration(b *testing.B) {
log.SetLevel(log.ErrorLevel)
worldState := common.WorldOfTime(time.Unix(10, 0))
lcc, rcc, ai := generateClientConfigs(basicTCPConfig, worldState)
rcc, ai := generateClientConfigs(basicTCPConfig, worldState)
sta := basicServerState(worldState)
const bufSize = 16 * 1024
@ -527,7 +541,7 @@ func BenchmarkIntegration(b *testing.B) {
for name, method := range encryptionMethods {
b.Run(name, func(b *testing.B) {
ai.EncryptionMethod = method
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(lcc, rcc, ai, sta)
proxyToCkClientD, proxyFromCkServerL, _, _, err := establishSession(rcc, ai, sta, false)
if err != nil {
b.Fatal(err)
}

@ -0,0 +1,52 @@
package browsers
import (
"encoding/binary"
"encoding/hex"
)
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
}
// Browser represents the signature of a browser at a particular version
type Browser interface {
// ComposeClientHello produces the ClientHello message (without TLS record layer) as the mimicking browser would
ComposeClientHello(ClientHelloFields) []byte
}
// 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
}
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
}

@ -1,4 +1,4 @@
package client
package browsers
import (
"encoding/hex"

@ -1,6 +1,6 @@
// Fingerprint of Chrome 112
package client
package browsers
import (
"encoding/binary"
@ -94,20 +94,20 @@ func (c *Chrome) composeExtensions(serverName string, keyShare []byte) []byte {
return ret
}
func (c *Chrome) composeClientHello(hd clientHelloFields) (ch []byte) {
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[3] = hd.Random // random
clientHello[4] = []byte{0x20} // session id length 32
clientHello[5] = hd.sessionId // session id
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)
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

@ -1,4 +1,4 @@
package client
package browsers
import (
"encoding/hex"
@ -27,7 +27,7 @@ func TestMakeGREASE(t *testing.T) {
}
//func TestChromeJA3(t *testing.T) {
// result := common.AddRecordLayer((&Chrome{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
// result := common.AddRecordLayer((&Chrome{}).ComposeClientHello(hd), common.Handshake, common.VersionTLS11)
// assert.Equal(t, 517, len(result))
//
// hello := tlsx.ClientHelloBasic{}

@ -1,6 +1,6 @@
// Fingerprint of Firefox 112
package client
package browsers
import (
"encoding/binary"
@ -47,20 +47,20 @@ func (f *Firefox) composeExtensions(serverName string, keyShare []byte) []byte {
return ret
}
func (f *Firefox) composeClientHello(hd clientHelloFields) (ch []byte) {
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[3] = hd.Random // random
clientHello[4] = []byte{0x20} // session id length 32
clientHello[5] = hd.sessionId // session id
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)
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

@ -1,4 +1,4 @@
package client
package browsers
import (
"encoding/hex"
@ -7,15 +7,15 @@ import (
"testing"
)
var hd = clientHelloFields{
random: decodeHex("ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a0"),
sessionId: decodeHex("47485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b9"),
x25519KeyShare: decodeHex("d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa44"),
serverName: "github.com",
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)
// result := common.AddRecordLayer((&Firefox{}).ComposeClientHello(hd), common.Handshake, common.VersionTLS11)
//
// hello := tlsx.ClientHelloBasic{}
// err := hello.Unmarshal(result)
@ -26,7 +26,7 @@ var hd = clientHelloFields{
//}
func TestFirefoxComposeClientHello(t *testing.T) {
result := hex.EncodeToString((&Firefox{}).composeClientHello(hd))
result := hex.EncodeToString((&Firefox{}).ComposeClientHello(hd))
target := "010001fc0303ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a02047485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b90022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035010001910000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b000201000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d0020d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa440017004104c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47002b00050403040303000d0018001604030503060308040805080604010501060102030201001c0002400100150096000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
// skip random secp256r1

@ -1,6 +1,6 @@
// Fingerprint of Safari 16.4
package client
package browsers
import (
"encoding/binary"
@ -62,20 +62,20 @@ func (s *Safari) composeExtensions(serverName string, keyShare []byte) []byte {
return ret
}
func (s *Safari) composeClientHello(hd clientHelloFields) (ch []byte) {
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[3] = hd.Random // random
clientHello[4] = []byte{0x20} // session id length 32
clientHello[5] = hd.sessionId // session id
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)
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

@ -1,14 +1,14 @@
package client
package browsers
import (
"testing"
)
var safariHd = clientHelloFields{
random: decodeHex("977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc17"),
sessionId: decodeHex("c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3"),
x25519KeyShare: decodeHex("c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a"),
serverName: "github.com",
var safariHd = ClientHelloFields{
Random: decodeHex("977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc17"),
SessionId: decodeHex("c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3"),
X25519KeyShare: decodeHex("c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a"),
ServerName: "github.com",
}
//func TestSafariJA3(t *testing.T) {

@ -0,0 +1,205 @@
package client
import (
"fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client/browsers"
"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 "direct", "":
var browser browsers.Browser
switch strings.ToLower(raw.BrowserSig) {
case "chrome", "":
browser = &browsers.Chrome{}
case "firefox":
browser = &browsers.Firefox{}
default:
err = fmt.Errorf("unknown browser signature %v", raw.BrowserSig)
return
}
remote.TransportMaker = func() transports.Transport {
return &transports.DirectTLS{
Browser: browser,
}
}
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,
}
}
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"
)
// On different invocations to MakeSession, authInfo.SessionId MUST be different
func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.Dialer) *mux.Session {
type CloakClient struct {
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")
connsCh := make(chan net.Conn, connConfig.NumConn)
@ -55,19 +65,37 @@ func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.D
}
seshConfig := mux.SessionConfig{
Singleplex: connConfig.Singleplex,
Obfuscator: obfuscator,
Valve: nil,
Unordered: authInfo.Unordered,
MsgOnWireSizeLimit: appDataMaxLength,
InactivityTimeout: connConfig.InactivityTimeout,
}
sesh := mux.MakeSession(authInfo.SessionId, seshConfig)
session := mux.MakeSession(authInfo.SessionId, seshConfig)
for i := 0; i < connConfig.NumConn; i++ {
conn := <-connsCh
sesh.AddConnection(conn)
session.AddConnection(conn)
}
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,62 @@
package transports
import (
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/libcloak/client/browsers"
log "github.com/sirupsen/logrus"
"net"
)
type DirectTLS struct {
*common.TLSConn
Browser browsers.Browser
}
// 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)
// random is marshalled ephemeral pub key 32 bytes
// The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare
fields := browsers.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,4 +1,4 @@
package client
package transports
import (
"encoding/binary"

@ -1,4 +1,4 @@
package client
package transports
import (
"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 (
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"github.com/cbeuw/Cloak/internal/common"
"github.com/gorilla/websocket"
utls "gitlab.com/yawning/utls.git"
"net"
"net/http"
"net/url"
)
type WSOverTLS struct {
*common.WebSocketConn
wsUrl string
CDNHost string
CDNPort string
}
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
}
u, err := url.Parse(ws.wsUrl)
u, err := url.Parse("ws://" + net.JoinHostPort(ws.CDNHost, ws.CDNPort))
if err != nil {
return sessionKey, fmt.Errorf("failed to parse ws url: %v", err)
}
Loading…
Cancel
Save