├── internal ├── test │ └── test.go ├── common │ ├── dialer.go │ ├── worldstate.go │ ├── tls_test.go │ ├── websocket.go │ ├── crypto.go │ ├── copy.go │ ├── tls.go │ └── crypto_test.go ├── multiplex │ ├── frame.go │ ├── session_fuzz.go │ ├── recvBuffer.go │ ├── datagramBufferedPipe_test.go │ ├── qos.go │ ├── streamBufferedPipe_test.go │ ├── streamBufferedPipe.go │ ├── streamBuffer_test.go │ ├── datagramBufferedPipe.go │ ├── streamBuffer.go │ ├── mux_test.go │ ├── switchboard_test.go │ ├── switchboard.go │ ├── stream.go │ ├── obfs.go │ └── obfs_test.go ├── server │ ├── transport.go │ ├── usermanager │ │ ├── voidmanager.go │ │ ├── voidmanager_test.go │ │ ├── usermanager.go │ │ ├── api_router.go │ │ ├── api.yaml │ │ └── api_router_test.go │ ├── first_packet_fuzz.go │ ├── websocketAux_test.go │ ├── activeuser.go │ ├── auth.go │ ├── websocket.go │ ├── TLS.go │ ├── state_test.go │ ├── activeuser_test.go │ ├── TLSAux_test.go │ ├── userpanel_test.go │ ├── state.go │ ├── websocketAux.go │ ├── TLSAux.go │ └── userpanel.go ├── client │ ├── transport.go │ ├── state_test.go │ ├── auth.go │ ├── websocket.go │ ├── auth_test.go │ ├── connector.go │ ├── piper.go │ └── TLS.go └── ecdh │ ├── curve25519.go │ └── curve25519_test.go ├── codecov.yaml ├── .gitignore ├── Dockerfile ├── cmd ├── ck-client │ ├── log.go │ ├── protector.go │ ├── log_android.go │ ├── protector_android.go │ └── ck-client.go └── ck-server │ ├── keygen.go │ ├── ck-server_test.go │ └── ck-server.go ├── renovate.json ├── example_config ├── ckclient.json └── ckserver.json ├── Makefile ├── go.mod ├── release.sh ├── .github └── workflows │ ├── release.yml │ └── build.yml └── go.sum /internal/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | corpus/ 2 | suppressions/ 3 | crashers/ 4 | *.zip 5 | .idea/ 6 | build/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | RUN git clone https://github.com/cbeuw/Cloak.git 4 | WORKDIR Cloak 5 | RUN make 6 | -------------------------------------------------------------------------------- /cmd/ck-client/log.go: -------------------------------------------------------------------------------- 1 | //go:build !android 2 | // +build !android 3 | 4 | package main 5 | 6 | func log_init() { 7 | } 8 | -------------------------------------------------------------------------------- /internal/common/dialer.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "net" 4 | 5 | type Dialer interface { 6 | Dial(network, address string) (net.Conn, error) 7 | } 8 | -------------------------------------------------------------------------------- /cmd/ck-client/protector.go: -------------------------------------------------------------------------------- 1 | //go:build !android 2 | // +build !android 3 | 4 | package main 5 | 6 | import "syscall" 7 | 8 | func protector(string, string, syscall.RawConn) error { 9 | return nil 10 | } 11 | -------------------------------------------------------------------------------- /internal/multiplex/frame.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | const ( 4 | closingNothing = iota 5 | closingStream 6 | closingSession 7 | ) 8 | 9 | type Frame struct { 10 | StreamID uint32 11 | Seq uint64 12 | Closing uint8 13 | Payload []byte 14 | } 15 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "packagePatterns": ["*"], 9 | "excludePackagePatterns": ["utls"], 10 | "enabled": false 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /example_config/ckclient.json: -------------------------------------------------------------------------------- 1 | { 2 | "Transport": "direct", 3 | "ProxyMethod": "shadowsocks", 4 | "EncryptionMethod": "plain", 5 | "UID": "---Your UID here---", 6 | "PublicKey": "---Public key here---", 7 | "ServerName": "www.bing.com", 8 | "NumConn": 4, 9 | "BrowserSig": "chrome", 10 | "StreamTimeout": 300 11 | } 12 | -------------------------------------------------------------------------------- /internal/common/worldstate.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | "time" 7 | ) 8 | 9 | var RealWorldState = WorldState{ 10 | Rand: rand.Reader, 11 | Now: time.Now, 12 | } 13 | 14 | type WorldState struct { 15 | Rand io.Reader 16 | Now func() time.Time 17 | } 18 | 19 | func WorldOfTime(t time.Time) WorldState { 20 | return WorldState{ 21 | Rand: rand.Reader, 22 | Now: func() time.Time { return t }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/server/transport.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto" 5 | "errors" 6 | "io" 7 | "net" 8 | ) 9 | 10 | type Responder = func(originalConn net.Conn, sessionKey [32]byte, randSource io.Reader) (preparedConn net.Conn, err error) 11 | type Transport interface { 12 | processFirstPacket(reqPacket []byte, privateKey crypto.PrivateKey) (authFragments, Responder, error) 13 | } 14 | 15 | var ErrInvalidPubKey = errors.New("public key has invalid format") 16 | var ErrCiphertextLength = errors.New("ciphertext has the wrong length") 17 | -------------------------------------------------------------------------------- /internal/multiplex/session_fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package multiplex 5 | 6 | func setupSesh_fuzz(unordered bool) *Session { 7 | obfuscator, _ := MakeObfuscator(EncryptionMethodPlain, [32]byte{}) 8 | 9 | seshConfig := SessionConfig{ 10 | Obfuscator: obfuscator, 11 | Valve: nil, 12 | Unordered: unordered, 13 | } 14 | return MakeSession(0, seshConfig) 15 | } 16 | 17 | func Fuzz(data []byte) int { 18 | sesh := setupSesh_fuzz(false) 19 | err := sesh.recvDataFromRemote(data) 20 | if err == nil { 21 | return 1 22 | } 23 | return 0 24 | } 25 | -------------------------------------------------------------------------------- /example_config/ckserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProxyBook": { 3 | "shadowsocks": [ 4 | "tcp", 5 | "127.0.0.1:8388" 6 | ], 7 | "openvpn": [ 8 | "udp", 9 | "127.0.0.1:8389" 10 | ], 11 | "tor": [ 12 | "tcp", 13 | "127.0.0.1:9001" 14 | ] 15 | }, 16 | "BindAddr": [ 17 | ":443", 18 | ":80" 19 | ], 20 | "BypassUID": [ 21 | "---Bypass UID here---" 22 | ], 23 | "RedirAddr": "cloudflare.com", 24 | "PrivateKey": "---Private key here---", 25 | "AdminUID": "---Admin UID here (optional)---", 26 | "DatabasePath": "userinfo.db" 27 | } 28 | -------------------------------------------------------------------------------- /internal/client/transport.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type Transport interface { 8 | Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) 9 | net.Conn 10 | } 11 | 12 | type TransportConfig struct { 13 | mode string 14 | 15 | wsUrl string 16 | 17 | browser browser 18 | } 19 | 20 | func (t TransportConfig) CreateTransport() Transport { 21 | switch t.mode { 22 | case "cdn": 23 | return &WSOverTLS{ 24 | wsUrl: t.wsUrl, 25 | } 26 | case "direct": 27 | return &DirectTLS{ 28 | browser: t.browser, 29 | } 30 | default: 31 | return nil 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/ck-server/keygen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | 7 | "github.com/cbeuw/Cloak/internal/common" 8 | "github.com/cbeuw/Cloak/internal/ecdh" 9 | ) 10 | 11 | func generateUID() string { 12 | UID := make([]byte, 16) 13 | common.CryptoRandRead(UID) 14 | return base64.StdEncoding.EncodeToString(UID) 15 | } 16 | 17 | func generateKeyPair() (string, string) { 18 | staticPv, staticPub, _ := ecdh.GenerateKey(rand.Reader) 19 | marshPub := ecdh.Marshal(staticPub) 20 | marshPv := staticPv.(*[32]byte)[:] 21 | return base64.StdEncoding.EncodeToString(marshPub), base64.StdEncoding.EncodeToString(marshPv) 22 | } 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | version=$(shell ver=$$(git log -n 1 --pretty=oneline --format=%D | awk -F, '{print $$1}' | awk '{print $$3}'); \ 4 | if [ "$$ver" = "master" ] ; then \ 5 | ver="master($$(git log -n 1 --pretty=oneline --format=%h))" ; \ 6 | fi ; \ 7 | echo $$ver) 8 | 9 | client: 10 | mkdir -p build 11 | go build -ldflags "-X main.version=${version}" ./cmd/ck-client 12 | mv ck-client* ./build 13 | 14 | server: 15 | mkdir -p build 16 | go build -ldflags "-X main.version=${version}" ./cmd/ck-server 17 | mv ck-server* ./build 18 | 19 | install: 20 | mv build/ck-* /usr/local/bin 21 | 22 | all: client server 23 | 24 | clean: 25 | rm -rf ./build/ck-* 26 | -------------------------------------------------------------------------------- /internal/multiplex/recvBuffer.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "time" 7 | ) 8 | 9 | var ErrTimeout = errors.New("deadline exceeded") 10 | 11 | type recvBuffer interface { 12 | // Read calls' err must be nil | io.EOF | io.ErrShortBuffer 13 | // Read should NOT return error on a closed streamBuffer with a non-empty buffer. 14 | // Instead, it should behave as if it hasn't been closed. Closure is only relevant 15 | // when the buffer is empty. 16 | io.ReadCloser 17 | Write(*Frame) (toBeClosed bool, err error) 18 | SetReadDeadline(time time.Time) 19 | } 20 | 21 | // size we want the amount of unread data in buffer to grow before recvBuffer.Write blocks. 22 | // If the buffer grows larger than what the system's memory can offer at the time of recvBuffer.Write, 23 | // a panic will happen. 24 | const recvBufferSizeLimit = 1<<31 - 1 25 | -------------------------------------------------------------------------------- /internal/server/usermanager/voidmanager.go: -------------------------------------------------------------------------------- 1 | package usermanager 2 | 3 | type Voidmanager struct{} 4 | 5 | func (v *Voidmanager) AuthenticateUser(bytes []byte) (int64, int64, error) { 6 | return 0, 0, ErrMangerIsVoid 7 | } 8 | 9 | func (v *Voidmanager) AuthoriseNewSession(bytes []byte, info AuthorisationInfo) error { 10 | return ErrMangerIsVoid 11 | } 12 | 13 | func (v *Voidmanager) UploadStatus(updates []StatusUpdate) ([]StatusResponse, error) { 14 | return nil, ErrMangerIsVoid 15 | } 16 | 17 | func (v *Voidmanager) ListAllUsers() ([]UserInfo, error) { 18 | return []UserInfo{}, ErrMangerIsVoid 19 | } 20 | 21 | func (v *Voidmanager) GetUserInfo(UID []byte) (UserInfo, error) { 22 | return UserInfo{}, ErrMangerIsVoid 23 | } 24 | 25 | func (v *Voidmanager) WriteUserInfo(info UserInfo) error { 26 | return ErrMangerIsVoid 27 | } 28 | 29 | func (v *Voidmanager) DeleteUser(UID []byte) error { 30 | return ErrMangerIsVoid 31 | } 32 | -------------------------------------------------------------------------------- /internal/common/tls_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkTLSConn_Write(b *testing.B) { 9 | const bufSize = 16 * 1024 10 | addrCh := make(chan string, 1) 11 | go func() { 12 | listener, err := net.Listen("tcp", "127.0.0.1:0") 13 | if err != nil { 14 | b.Fatal(err) 15 | } 16 | addrCh <- listener.Addr().String() 17 | conn, err := listener.Accept() 18 | if err != nil { 19 | b.Fatal(err) 20 | } 21 | readBuf := make([]byte, bufSize*2) 22 | for { 23 | _, err = conn.Read(readBuf) 24 | if err != nil { 25 | return 26 | } 27 | } 28 | }() 29 | data := make([]byte, bufSize) 30 | discardConn, _ := net.Dial("tcp", <-addrCh) 31 | tlsConn := NewTLSConn(discardConn) 32 | defer tlsConn.Close() 33 | b.SetBytes(bufSize) 34 | b.ResetTimer() 35 | b.RunParallel(func(pb *testing.PB) { 36 | for pb.Next() { 37 | tlsConn.Write(data) 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cbeuw/Cloak 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 9 | github.com/gorilla/mux v1.8.1 10 | github.com/gorilla/websocket v1.5.3 11 | github.com/juju/ratelimit v1.0.2 12 | github.com/refraction-networking/utls v1.8.0 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/stretchr/testify v1.10.0 15 | go.etcd.io/bbolt v1.4.0 16 | golang.org/x/crypto v0.37.0 17 | ) 18 | 19 | require ( 20 | github.com/andybalholm/brotli v1.1.1 // indirect 21 | github.com/cloudflare/circl v1.6.1 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/klauspost/compress v1.18.0 // indirect 24 | github.com/kr/pretty v0.3.1 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/rogpeppe/go-internal v1.14.1 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | go install github.com/mitchellh/gox@latest 6 | 7 | mkdir -p release 8 | 9 | rm -f ./release/* 10 | 11 | if [ -z "$v" ]; then 12 | echo "Version number cannot be null. Run with v=[version] release.sh" 13 | exit 1 14 | fi 15 | 16 | output="{{.Dir}}-{{.OS}}-{{.Arch}}-$v" 17 | osarch="!darwin/arm !darwin/386" 18 | 19 | echo "Compiling:" 20 | 21 | os="windows linux darwin" 22 | arch="amd64 386 arm arm64 mips mips64 mipsle mips64le" 23 | pushd cmd/ck-client 24 | CGO_ENABLED=0 gox -ldflags "-X main.version=${v}" -os="$os" -arch="$arch" -osarch="$osarch" -output="$output" 25 | CGO_ENABLED=0 GOOS="linux" GOARCH="mips" GOMIPS="softfloat" go build -ldflags "-X main.version=${v}" -o ck-client-linux-mips_softfloat-"${v}" 26 | CGO_ENABLED=0 GOOS="linux" GOARCH="mipsle" GOMIPS="softfloat" go build -ldflags "-X main.version=${v}" -o ck-client-linux-mipsle_softfloat-"${v}" 27 | mv ck-client-* ../../release 28 | popd 29 | 30 | os="linux" 31 | arch="amd64 386 arm arm64" 32 | pushd cmd/ck-server 33 | CGO_ENABLED=0 gox -ldflags "-X main.version=${v}" -os="$os" -arch="$arch" -osarch="$osarch" -output="$output" 34 | mv ck-server-* ../../release 35 | popd 36 | 37 | sha256sum release/* -------------------------------------------------------------------------------- /internal/server/usermanager/voidmanager_test.go: -------------------------------------------------------------------------------- 1 | package usermanager 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var v = &Voidmanager{} 10 | 11 | func Test_Voidmanager_AuthenticateUser(t *testing.T) { 12 | _, _, err := v.AuthenticateUser([]byte{}) 13 | assert.Equal(t, ErrMangerIsVoid, err) 14 | } 15 | 16 | func Test_Voidmanager_AuthoriseNewSession(t *testing.T) { 17 | err := v.AuthoriseNewSession([]byte{}, AuthorisationInfo{}) 18 | assert.Equal(t, ErrMangerIsVoid, err) 19 | } 20 | 21 | func Test_Voidmanager_DeleteUser(t *testing.T) { 22 | err := v.DeleteUser([]byte{}) 23 | assert.Equal(t, ErrMangerIsVoid, err) 24 | } 25 | 26 | func Test_Voidmanager_GetUserInfo(t *testing.T) { 27 | _, err := v.GetUserInfo([]byte{}) 28 | assert.Equal(t, ErrMangerIsVoid, err) 29 | } 30 | 31 | func Test_Voidmanager_ListAllUsers(t *testing.T) { 32 | _, err := v.ListAllUsers() 33 | assert.Equal(t, ErrMangerIsVoid, err) 34 | } 35 | 36 | func Test_Voidmanager_UploadStatus(t *testing.T) { 37 | _, err := v.UploadStatus([]StatusUpdate{}) 38 | assert.Equal(t, ErrMangerIsVoid, err) 39 | } 40 | 41 | func Test_Voidmanager_WriteUserInfo(t *testing.T) { 42 | err := v.WriteUserInfo(UserInfo{}) 43 | assert.Equal(t, ErrMangerIsVoid, err) 44 | } 45 | -------------------------------------------------------------------------------- /internal/client/state_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseConfig(t *testing.T) { 11 | ssv := "UID=iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=;PublicKey=IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=;" + 12 | "ServerName=www.bing.com;NumConn=4;MaskBrowser=chrome;ProxyMethod=shadowsocks;EncryptionMethod=plain" 13 | json := ssvToJson(ssv) 14 | expected := []byte(`{"UID":"iGAO85zysIyR4c09CyZSLdNhtP/ckcYu7nIPI082AHA=","PublicKey":"IYoUzkle/T/kriE+Ufdm7AHQtIeGnBWbhhlTbmDpUUI=","ServerName":"www.bing.com","NumConn":4,"MaskBrowser":"chrome","ProxyMethod":"shadowsocks","EncryptionMethod":"plain"}`) 15 | 16 | t.Run("byte equality", func(t *testing.T) { 17 | assert.Equal(t, expected, json) 18 | }) 19 | 20 | t.Run("struct equality", func(t *testing.T) { 21 | tmpConfig, _ := ioutil.TempFile("", "ck_client_config") 22 | _, _ = tmpConfig.Write(expected) 23 | parsedFromSSV, err := ParseConfig(ssv) 24 | assert.NoError(t, err) 25 | parsedFromJson, err := ParseConfig(tmpConfig.Name()) 26 | assert.NoError(t, err) 27 | 28 | assert.Equal(t, parsedFromJson, parsedFromSSV) 29 | }) 30 | 31 | t.Run("empty file", func(t *testing.T) { 32 | tmpConfig, _ := ioutil.TempFile("", "ck_client_config") 33 | _, err := ParseConfig(tmpConfig.Name()) 34 | assert.Error(t, err) 35 | }) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /internal/server/first_packet_fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package server 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "time" 10 | 11 | "github.com/cbeuw/Cloak/internal/common" 12 | "github.com/cbeuw/connutil" 13 | ) 14 | 15 | type rfpReturnValue_fuzz struct { 16 | n int 17 | transport Transport 18 | redirOnErr bool 19 | err error 20 | } 21 | 22 | func Fuzz(data []byte) int { 23 | var bypassUID [16]byte 24 | 25 | var pv [32]byte 26 | 27 | sta := &State{ 28 | BypassUID: map[[16]byte]struct{}{ 29 | bypassUID: {}, 30 | }, 31 | ProxyBook: map[string]net.Addr{ 32 | "shadowsocks": nil, 33 | }, 34 | UsedRandom: map[[32]byte]int64{}, 35 | StaticPv: &pv, 36 | WorldState: common.RealWorldState, 37 | } 38 | 39 | rfp := func(conn net.Conn, buf []byte, retChan chan<- rfpReturnValue_fuzz) { 40 | ret := rfpReturnValue_fuzz{} 41 | ret.n, ret.transport, ret.redirOnErr, ret.err = readFirstPacket(conn, buf, 500*time.Millisecond) 42 | retChan <- ret 43 | } 44 | 45 | local, remote := connutil.AsyncPipe() 46 | buf := make([]byte, 1500) 47 | retChan := make(chan rfpReturnValue_fuzz) 48 | go rfp(remote, buf, retChan) 49 | 50 | local.Write(data) 51 | 52 | ret := <-retChan 53 | 54 | if ret.err != nil { 55 | return 1 56 | } 57 | 58 | _, _, err := AuthFirstPacket(buf[:ret.n], ret.transport, sta) 59 | 60 | if !errors.Is(err, ErrReplay) && !errors.Is(err, ErrBadDecryption) { 61 | return 1 62 | } 63 | return 0 64 | } 65 | -------------------------------------------------------------------------------- /internal/server/websocketAux_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/cbeuw/connutil" 8 | ) 9 | 10 | func TestFirstBuffedConn_Read(t *testing.T) { 11 | mockConn, writingEnd := connutil.AsyncPipe() 12 | 13 | expectedFirstPacket := []byte{1, 2, 3} 14 | firstBuffedConn := &firstBuffedConn{ 15 | Conn: mockConn, 16 | firstRead: false, 17 | firstPacket: expectedFirstPacket, 18 | } 19 | 20 | buf := make([]byte, 1024) 21 | n, err := firstBuffedConn.Read(buf) 22 | if err != nil { 23 | t.Error(err) 24 | return 25 | } 26 | if !bytes.Equal(expectedFirstPacket, buf[:n]) { 27 | t.Error("first read doesn't produce given packet") 28 | return 29 | } 30 | 31 | expectedSecondPacket := []byte{4, 5, 6, 7} 32 | writingEnd.Write(expectedSecondPacket) 33 | n, err = firstBuffedConn.Read(buf) 34 | if err != nil { 35 | t.Error(err) 36 | return 37 | } 38 | if !bytes.Equal(expectedSecondPacket, buf[:n]) { 39 | t.Error("second read doesn't produce subsequently written packet") 40 | return 41 | } 42 | } 43 | 44 | func TestWsAcceptor(t *testing.T) { 45 | mockConn := connutil.Discard() 46 | expectedFirstPacket := []byte{1, 2, 3} 47 | 48 | wsAcceptor := newWsAcceptor(mockConn, expectedFirstPacket) 49 | _, err := wsAcceptor.Accept() 50 | if err != nil { 51 | t.Error(err) 52 | return 53 | } 54 | 55 | _, err = wsAcceptor.Accept() 56 | if err == nil { 57 | t.Error("accepting second time doesn't return error") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Build 14 | run: | 15 | export PATH=${PATH}:`go env GOPATH`/bin 16 | v=${GITHUB_REF#refs/*/} ./release.sh 17 | - name: Release 18 | uses: softprops/action-gh-release@v1 19 | with: 20 | files: release/* 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | build-docker: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | - name: Docker meta 30 | id: meta 31 | uses: docker/metadata-action@v5 32 | with: 33 | images: | 34 | cbeuw/cloak 35 | tags: | 36 | type=ref,event=branch 37 | type=ref,event=pr 38 | type=semver,pattern={{version}} 39 | type=semver,pattern={{major}}.{{minor}} 40 | - name: Login to Docker Hub 41 | uses: docker/login-action@v3 42 | with: 43 | username: ${{ secrets.DOCKERHUB_USERNAME }} 44 | password: ${{ secrets.DOCKERHUB_TOKEN }} 45 | - name: Build and push 46 | uses: docker/build-push-action@v6 47 | with: 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | -------------------------------------------------------------------------------- /internal/server/usermanager/usermanager.go: -------------------------------------------------------------------------------- 1 | package usermanager 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type StatusUpdate struct { 8 | UID []byte 9 | Active bool 10 | NumSession int 11 | 12 | UpUsage int64 13 | DownUsage int64 14 | Timestamp int64 15 | } 16 | 17 | type MaybeInt32 *int32 18 | type MaybeInt64 *int64 19 | 20 | type UserInfo struct { 21 | UID []byte 22 | SessionsCap MaybeInt32 23 | UpRate MaybeInt64 24 | DownRate MaybeInt64 25 | UpCredit MaybeInt64 26 | DownCredit MaybeInt64 27 | ExpiryTime MaybeInt64 28 | } 29 | 30 | func JustInt32(v int32) MaybeInt32 { return &v } 31 | 32 | func JustInt64(v int64) MaybeInt64 { return &v } 33 | 34 | type StatusResponse struct { 35 | UID []byte 36 | Action int 37 | Message string 38 | } 39 | 40 | type AuthorisationInfo struct { 41 | NumExistingSessions int 42 | } 43 | 44 | const ( 45 | TERMINATE = iota + 1 46 | ) 47 | 48 | var ErrUserNotFound = errors.New("UID does not correspond to a user") 49 | var ErrSessionsCapReached = errors.New("Sessions cap has reached") 50 | var ErrMangerIsVoid = errors.New("cannot perform operation with user manager as database path is not specified") 51 | 52 | var ErrNoUpCredit = errors.New("No upload credit left") 53 | var ErrNoDownCredit = errors.New("No download credit left") 54 | var ErrUserExpired = errors.New("User has expired") 55 | 56 | type UserManager interface { 57 | AuthenticateUser([]byte) (int64, int64, error) 58 | AuthoriseNewSession([]byte, AuthorisationInfo) error 59 | UploadStatus([]StatusUpdate) ([]StatusResponse, error) 60 | ListAllUsers() ([]UserInfo, error) 61 | GetUserInfo(UID []byte) (UserInfo, error) 62 | WriteUserInfo(UserInfo) error 63 | DeleteUser(UID []byte) error 64 | } 65 | -------------------------------------------------------------------------------- /internal/common/websocket.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | // WebSocketConn implements io.ReadWriteCloser 13 | // it makes websocket.Conn binary-oriented 14 | type WebSocketConn struct { 15 | *websocket.Conn 16 | writeM sync.Mutex 17 | } 18 | 19 | func (ws *WebSocketConn) Write(data []byte) (int, error) { 20 | ws.writeM.Lock() 21 | err := ws.WriteMessage(websocket.BinaryMessage, data) 22 | ws.writeM.Unlock() 23 | if err != nil { 24 | return 0, err 25 | } else { 26 | return len(data), nil 27 | } 28 | } 29 | 30 | func (ws *WebSocketConn) Read(buf []byte) (n int, err error) { 31 | t, r, err := ws.NextReader() 32 | if err != nil { 33 | return 0, err 34 | } 35 | if t != websocket.BinaryMessage { 36 | return 0, nil 37 | } 38 | 39 | // Read until io.EOL for one full message 40 | for { 41 | var read int 42 | read, err = r.Read(buf[n:]) 43 | if err != nil { 44 | if err == io.EOF { 45 | err = nil 46 | break 47 | } else { 48 | break 49 | } 50 | } else { 51 | // There may be data available to read but n == len(buf)-1, read==0 because buffer is full 52 | if read == 0 { 53 | err = errors.New("nothing more is read. message may be larger than buffer") 54 | break 55 | } 56 | } 57 | n += read 58 | } 59 | return 60 | } 61 | func (ws *WebSocketConn) Close() error { 62 | ws.writeM.Lock() 63 | defer ws.writeM.Unlock() 64 | return ws.Conn.Close() 65 | } 66 | 67 | func (ws *WebSocketConn) SetDeadline(t time.Time) error { 68 | err := ws.SetReadDeadline(t) 69 | if err != nil { 70 | return err 71 | } 72 | err = ws.SetWriteDeadline(t) 73 | if err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /cmd/ck-client/log_android.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build android 6 | // +build android 7 | 8 | package main 9 | 10 | /* 11 | To view the log output run: 12 | adb logcat GoLog:I *:S 13 | */ 14 | 15 | // Android redirects stdout and stderr to /dev/null. 16 | // As these are common debugging utilities in Go, 17 | // we redirect them to logcat. 18 | // 19 | // Unfortunately, logcat is line oriented, so we must buffer. 20 | 21 | /* 22 | #cgo LDFLAGS: -landroid -llog 23 | 24 | #include 25 | #include 26 | #include 27 | */ 28 | import "C" 29 | 30 | import ( 31 | "bufio" 32 | "os" 33 | "unsafe" 34 | 35 | log "github.com/sirupsen/logrus" 36 | ) 37 | 38 | var ( 39 | ctag = C.CString("cloak") 40 | ) 41 | 42 | type infoWriter struct{} 43 | 44 | func (infoWriter) Write(p []byte) (n int, err error) { 45 | cstr := C.CString(string(p)) 46 | C.__android_log_write(C.ANDROID_LOG_INFO, ctag, cstr) 47 | C.free(unsafe.Pointer(cstr)) 48 | return len(p), nil 49 | } 50 | 51 | func lineLog(f *os.File, priority C.int) { 52 | const logSize = 1024 // matches android/log.h. 53 | r := bufio.NewReaderSize(f, logSize) 54 | for { 55 | line, _, err := r.ReadLine() 56 | str := string(line) 57 | if err != nil { 58 | str += " " + err.Error() 59 | } 60 | cstr := C.CString(str) 61 | C.__android_log_write(priority, ctag, cstr) 62 | C.free(unsafe.Pointer(cstr)) 63 | if err != nil { 64 | break 65 | } 66 | } 67 | } 68 | 69 | func log_init() { 70 | log.SetOutput(infoWriter{}) 71 | 72 | r, w, err := os.Pipe() 73 | if err != nil { 74 | panic(err) 75 | } 76 | os.Stderr = w 77 | go lineLog(r, C.ANDROID_LOG_ERROR) 78 | 79 | r, w, err = os.Pipe() 80 | if err != nil { 81 | panic(err) 82 | } 83 | os.Stdout = w 84 | go lineLog(r, C.ANDROID_LOG_INFO) 85 | } 86 | -------------------------------------------------------------------------------- /internal/multiplex/datagramBufferedPipe_test.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDatagramBuffer_RW(t *testing.T) { 11 | b := []byte{0x01, 0x02, 0x03} 12 | t.Run("simple write", func(t *testing.T) { 13 | pipe := NewDatagramBufferedPipe() 14 | _, err := pipe.Write(&Frame{Payload: b}) 15 | assert.NoError(t, err) 16 | }) 17 | 18 | t.Run("simple read", func(t *testing.T) { 19 | pipe := NewDatagramBufferedPipe() 20 | _, _ = pipe.Write(&Frame{Payload: b}) 21 | b2 := make([]byte, len(b)) 22 | n, err := pipe.Read(b2) 23 | assert.NoError(t, err) 24 | assert.Equal(t, len(b), n) 25 | assert.Equal(t, b, b2) 26 | assert.Equal(t, 0, pipe.buf.Len(), "buf len is not 0 after finished reading") 27 | }) 28 | 29 | t.Run("writing closing frame", func(t *testing.T) { 30 | pipe := NewDatagramBufferedPipe() 31 | toBeClosed, err := pipe.Write(&Frame{Closing: closingStream}) 32 | assert.NoError(t, err) 33 | assert.True(t, toBeClosed, "should be to be closed") 34 | assert.True(t, pipe.closed, "pipe should be closed") 35 | }) 36 | } 37 | 38 | func TestDatagramBuffer_BlockingRead(t *testing.T) { 39 | pipe := NewDatagramBufferedPipe() 40 | b := []byte{0x01, 0x02, 0x03} 41 | go func() { 42 | time.Sleep(readBlockTime) 43 | pipe.Write(&Frame{Payload: b}) 44 | }() 45 | b2 := make([]byte, len(b)) 46 | n, err := pipe.Read(b2) 47 | assert.NoError(t, err) 48 | assert.Equal(t, len(b), n, "number of bytes read after block is wrong") 49 | assert.Equal(t, b, b2) 50 | } 51 | 52 | func TestDatagramBuffer_CloseThenRead(t *testing.T) { 53 | pipe := NewDatagramBufferedPipe() 54 | b := []byte{0x01, 0x02, 0x03} 55 | pipe.Write(&Frame{Payload: b}) 56 | b2 := make([]byte, len(b)) 57 | pipe.Close() 58 | n, err := pipe.Read(b2) 59 | assert.NoError(t, err) 60 | assert.Equal(t, len(b), n, "number of bytes read after block is wrong") 61 | assert.Equal(t, b, b2) 62 | } 63 | -------------------------------------------------------------------------------- /internal/multiplex/qos.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/juju/ratelimit" 7 | ) 8 | 9 | // Valve needs to be universal, across all sessions that belong to a user 10 | type LimitedValve struct { 11 | // traffic directions from the server's perspective are referred 12 | // exclusively as rx and tx. 13 | // rx is from client to server, tx is from server to client 14 | // DO NOT use terms up or down as this is used in usermanager 15 | // for bandwidth limiting 16 | rxtb *ratelimit.Bucket 17 | txtb *ratelimit.Bucket 18 | 19 | rx *int64 20 | tx *int64 21 | } 22 | 23 | type UnlimitedValve struct{} 24 | 25 | func MakeValve(rxRate, txRate int64) *LimitedValve { 26 | var rx, tx int64 27 | v := &LimitedValve{ 28 | rxtb: ratelimit.NewBucketWithRate(float64(rxRate), rxRate), 29 | txtb: ratelimit.NewBucketWithRate(float64(txRate), txRate), 30 | rx: &rx, 31 | tx: &tx, 32 | } 33 | return v 34 | } 35 | 36 | var UNLIMITED_VALVE = &UnlimitedValve{} 37 | 38 | func (v *LimitedValve) rxWait(n int) { v.rxtb.Wait(int64(n)) } 39 | func (v *LimitedValve) txWait(n int) { v.txtb.Wait(int64(n)) } 40 | func (v *LimitedValve) AddRx(n int64) { atomic.AddInt64(v.rx, n) } 41 | func (v *LimitedValve) AddTx(n int64) { atomic.AddInt64(v.tx, n) } 42 | func (v *LimitedValve) GetRx() int64 { return atomic.LoadInt64(v.rx) } 43 | func (v *LimitedValve) GetTx() int64 { return atomic.LoadInt64(v.tx) } 44 | func (v *LimitedValve) Nullify() (int64, int64) { 45 | rx := atomic.SwapInt64(v.rx, 0) 46 | tx := atomic.SwapInt64(v.tx, 0) 47 | return rx, tx 48 | } 49 | 50 | func (v *UnlimitedValve) rxWait(n int) {} 51 | func (v *UnlimitedValve) txWait(n int) {} 52 | func (v *UnlimitedValve) AddRx(n int64) {} 53 | func (v *UnlimitedValve) AddTx(n int64) {} 54 | func (v *UnlimitedValve) GetRx() int64 { return 0 } 55 | func (v *UnlimitedValve) GetTx() int64 { return 0 } 56 | func (v *UnlimitedValve) Nullify() (int64, int64) { return 0, 0 } 57 | 58 | type Valve interface { 59 | rxWait(n int) 60 | txWait(n int) 61 | AddRx(n int64) 62 | AddTx(n int64) 63 | GetRx() int64 64 | GetTx() int64 65 | Nullify() (int64, int64) 66 | } 67 | -------------------------------------------------------------------------------- /internal/client/auth.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/cbeuw/Cloak/internal/common" 7 | "github.com/cbeuw/Cloak/internal/ecdh" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const ( 12 | UNORDERED_FLAG = 0x01 // 0000 0001 13 | ) 14 | 15 | type authenticationPayload struct { 16 | randPubKey [32]byte 17 | ciphertextWithTag [64]byte 18 | } 19 | 20 | // makeAuthenticationPayload generates the ephemeral key pair, calculates the shared secret, and then compose and 21 | // encrypt the authenticationPayload 22 | func makeAuthenticationPayload(authInfo AuthInfo) (ret authenticationPayload, sharedSecret [32]byte) { 23 | /* 24 | Authentication data: 25 | +----------+----------------+---------------------+-------------+--------------+--------+------------+ 26 | | _UID_ | _Proxy Method_ | _Encryption Method_ | _Timestamp_ | _Session Id_ | _Flag_ | _reserved_ | 27 | +----------+----------------+---------------------+-------------+--------------+--------+------------+ 28 | | 16 bytes | 12 bytes | 1 byte | 8 bytes | 4 bytes | 1 byte | 6 bytes | 29 | +----------+----------------+---------------------+-------------+--------------+--------+------------+ 30 | */ 31 | ephPv, ephPub, err := ecdh.GenerateKey(authInfo.WorldState.Rand) 32 | if err != nil { 33 | log.Panicf("failed to generate ephemeral key pair: %v", err) 34 | } 35 | copy(ret.randPubKey[:], ecdh.Marshal(ephPub)) 36 | 37 | plaintext := make([]byte, 48) 38 | copy(plaintext, authInfo.UID) 39 | copy(plaintext[16:28], authInfo.ProxyMethod) 40 | plaintext[28] = authInfo.EncryptionMethod 41 | binary.BigEndian.PutUint64(plaintext[29:37], uint64(authInfo.WorldState.Now().UTC().Unix())) 42 | binary.BigEndian.PutUint32(plaintext[37:41], authInfo.SessionId) 43 | 44 | if authInfo.Unordered { 45 | plaintext[41] |= UNORDERED_FLAG 46 | } 47 | 48 | secret, err := ecdh.GenerateSharedSecret(ephPv, authInfo.ServerPubKey) 49 | if err != nil { 50 | log.Panicf("error in generating shared secret: %v", err) 51 | } 52 | copy(sharedSecret[:], secret) 53 | ciphertextWithTag, _ := common.AESGCMEncrypt(ret.randPubKey[:12], sharedSecret[:], plaintext) 54 | copy(ret.ciphertextWithTag[:], ciphertextWithTag[:]) 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /internal/client/websocket.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/cbeuw/Cloak/internal/common" 12 | "github.com/gorilla/websocket" 13 | utls "github.com/refraction-networking/utls" 14 | ) 15 | 16 | type WSOverTLS struct { 17 | *common.WebSocketConn 18 | wsUrl string 19 | } 20 | 21 | func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) { 22 | utlsConfig := &utls.Config{ 23 | ServerName: authInfo.MockDomain, 24 | InsecureSkipVerify: true, 25 | } 26 | uconn := utls.UClient(rawConn, utlsConfig, utls.HelloChrome_Auto) 27 | err = uconn.BuildHandshakeState() 28 | if err != nil { 29 | return 30 | } 31 | for i, extension := range uconn.Extensions { 32 | _, ok := extension.(*utls.ALPNExtension) 33 | if ok { 34 | uconn.Extensions = append(uconn.Extensions[:i], uconn.Extensions[i+1:]...) 35 | break 36 | } 37 | } 38 | 39 | err = uconn.Handshake() 40 | if err != nil { 41 | return 42 | } 43 | 44 | u, err := url.Parse(ws.wsUrl) 45 | if err != nil { 46 | return sessionKey, fmt.Errorf("failed to parse ws url: %v", err) 47 | } 48 | 49 | payload, sharedSecret := makeAuthenticationPayload(authInfo) 50 | header := http.Header{} 51 | header.Add("hidden", base64.StdEncoding.EncodeToString(append(payload.randPubKey[:], payload.ciphertextWithTag[:]...))) 52 | c, _, err := websocket.NewClient(uconn, u, header, 16480, 16480) 53 | if err != nil { 54 | return sessionKey, fmt.Errorf("failed to handshake: %v", err) 55 | } 56 | 57 | ws.WebSocketConn = &common.WebSocketConn{Conn: c} 58 | 59 | buf := make([]byte, 128) 60 | n, err := ws.Read(buf) 61 | if err != nil { 62 | return sessionKey, fmt.Errorf("failed to read reply: %v", err) 63 | } 64 | 65 | if n != 60 { 66 | return sessionKey, errors.New("reply must be 60 bytes") 67 | } 68 | 69 | reply := buf[:60] 70 | sessionKeySlice, err := common.AESGCMDecrypt(reply[:12], sharedSecret[:], reply[12:]) 71 | if err != nil { 72 | return 73 | } 74 | copy(sessionKey[:], sessionKeySlice) 75 | 76 | return 77 | } 78 | 79 | func (ws *WSOverTLS) Close() error { 80 | if ws.WebSocketConn != nil { 81 | return ws.WebSocketConn.Close() 82 | } 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/server/activeuser.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/cbeuw/Cloak/internal/server/usermanager" 7 | 8 | mux "github.com/cbeuw/Cloak/internal/multiplex" 9 | ) 10 | 11 | type ActiveUser struct { 12 | panel *userPanel 13 | 14 | arrUID [16]byte 15 | 16 | valve mux.Valve 17 | 18 | bypass bool 19 | 20 | sessionsM sync.RWMutex 21 | sessions map[uint32]*mux.Session 22 | } 23 | 24 | // CloseSession closes a session and removes its reference from the user 25 | func (u *ActiveUser) CloseSession(sessionID uint32, reason string) { 26 | u.sessionsM.Lock() 27 | sesh, existing := u.sessions[sessionID] 28 | if existing { 29 | delete(u.sessions, sessionID) 30 | sesh.SetTerminalMsg(reason) 31 | sesh.Close() 32 | } 33 | remaining := len(u.sessions) 34 | u.sessionsM.Unlock() 35 | if remaining == 0 { 36 | u.panel.TerminateActiveUser(u, "no session left") 37 | } 38 | } 39 | 40 | // GetSession returns the reference to an existing session, or if one such session doesn't exist, it queries 41 | // the UserManager for the authorisation for a new session. If a new session is allowed, it creates this new session 42 | // and returns its reference 43 | func (u *ActiveUser) GetSession(sessionID uint32, config mux.SessionConfig) (sesh *mux.Session, existing bool, err error) { 44 | u.sessionsM.Lock() 45 | defer u.sessionsM.Unlock() 46 | if sesh = u.sessions[sessionID]; sesh != nil { 47 | return sesh, true, nil 48 | } else { 49 | if !u.bypass { 50 | ainfo := usermanager.AuthorisationInfo{NumExistingSessions: len(u.sessions)} 51 | err := u.panel.Manager.AuthoriseNewSession(u.arrUID[:], ainfo) 52 | if err != nil { 53 | return nil, false, err 54 | } 55 | } 56 | config.Valve = u.valve 57 | sesh = mux.MakeSession(sessionID, config) 58 | u.sessions[sessionID] = sesh 59 | return sesh, false, nil 60 | } 61 | } 62 | 63 | // closeAllSessions closes all sessions of this active user 64 | func (u *ActiveUser) closeAllSessions(reason string) { 65 | u.sessionsM.Lock() 66 | for sessionID, sesh := range u.sessions { 67 | sesh.SetTerminalMsg(reason) 68 | sesh.Close() 69 | delete(u.sessions, sessionID) 70 | } 71 | u.sessionsM.Unlock() 72 | } 73 | 74 | // NumSession returns the number of active sessions 75 | func (u *ActiveUser) NumSession() int { 76 | u.sessionsM.RLock() 77 | defer u.sessionsM.RUnlock() 78 | return len(u.sessions) 79 | } 80 | -------------------------------------------------------------------------------- /internal/common/crypto.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "errors" 8 | "io" 9 | "math/big" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func AESGCMEncrypt(nonce []byte, key []byte, plaintext []byte) ([]byte, error) { 16 | block, err := aes.NewCipher(key) 17 | if err != nil { 18 | return nil, err 19 | } 20 | aesgcm, err := cipher.NewGCM(block) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if len(nonce) != aesgcm.NonceSize() { 25 | // check here so it doesn't panic 26 | return nil, errors.New("incorrect nonce size") 27 | } 28 | 29 | return aesgcm.Seal(nil, nonce, plaintext, nil), nil 30 | } 31 | 32 | func AESGCMDecrypt(nonce []byte, key []byte, ciphertext []byte) ([]byte, error) { 33 | block, err := aes.NewCipher(key) 34 | if err != nil { 35 | return nil, err 36 | } 37 | aesgcm, err := cipher.NewGCM(block) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if len(nonce) != aesgcm.NonceSize() { 42 | // check here so it doesn't panic 43 | return nil, errors.New("incorrect nonce size") 44 | } 45 | plain, err := aesgcm.Open(nil, nonce, ciphertext, nil) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return plain, nil 50 | } 51 | 52 | func CryptoRandRead(buf []byte) { 53 | RandRead(rand.Reader, buf) 54 | } 55 | 56 | func backoff(f func() error) { 57 | err := f() 58 | if err == nil { 59 | return 60 | } 61 | waitDur := [10]time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 30 * time.Millisecond, 50 * time.Millisecond, 62 | 100 * time.Millisecond, 300 * time.Millisecond, 500 * time.Millisecond, 1 * time.Second, 63 | 3 * time.Second, 5 * time.Second} 64 | for i := 0; i < 10; i++ { 65 | log.Errorf("Failed to get random: %v. Retrying...", err) 66 | err = f() 67 | if err == nil { 68 | return 69 | } 70 | time.Sleep(waitDur[i]) 71 | } 72 | log.Fatal("Cannot get random after 10 retries") 73 | } 74 | 75 | func RandRead(randSource io.Reader, buf []byte) { 76 | backoff(func() error { 77 | _, err := randSource.Read(buf) 78 | return err 79 | }) 80 | } 81 | 82 | func RandItem[T any](list []T) T { 83 | return list[RandInt(len(list))] 84 | } 85 | 86 | func RandInt(n int) int { 87 | s := new(int) 88 | backoff(func() error { 89 | size, err := rand.Int(rand.Reader, big.NewInt(int64(n))) 90 | if err != nil { 91 | return err 92 | } 93 | *s = int(size.Int64()) 94 | return nil 95 | }) 96 | return *s 97 | } 98 | -------------------------------------------------------------------------------- /internal/multiplex/streamBufferedPipe_test.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const readBlockTime = 500 * time.Millisecond 12 | 13 | func TestPipeRW(t *testing.T) { 14 | pipe := NewStreamBufferedPipe() 15 | b := []byte{0x01, 0x02, 0x03} 16 | n, err := pipe.Write(b) 17 | assert.NoError(t, err, "simple write") 18 | assert.Equal(t, len(b), n, "number of bytes written") 19 | 20 | b2 := make([]byte, len(b)) 21 | n, err = pipe.Read(b2) 22 | assert.NoError(t, err, "simple read") 23 | assert.Equal(t, len(b), n, "number of bytes read") 24 | 25 | assert.Equal(t, b, b2) 26 | } 27 | 28 | func TestReadBlock(t *testing.T) { 29 | pipe := NewStreamBufferedPipe() 30 | b := []byte{0x01, 0x02, 0x03} 31 | go func() { 32 | time.Sleep(readBlockTime) 33 | pipe.Write(b) 34 | }() 35 | b2 := make([]byte, len(b)) 36 | n, err := pipe.Read(b2) 37 | assert.NoError(t, err, "blocked read") 38 | assert.Equal(t, len(b), n, "number of bytes read after block") 39 | 40 | assert.Equal(t, b, b2) 41 | } 42 | 43 | func TestPartialRead(t *testing.T) { 44 | pipe := NewStreamBufferedPipe() 45 | b := []byte{0x01, 0x02, 0x03} 46 | pipe.Write(b) 47 | b1 := make([]byte, 1) 48 | n, err := pipe.Read(b1) 49 | assert.NoError(t, err, "partial read of 1") 50 | assert.Equal(t, len(b1), n, "number of bytes in partial read of 1") 51 | 52 | assert.Equal(t, b[0], b1[0]) 53 | 54 | b2 := make([]byte, 2) 55 | n, err = pipe.Read(b2) 56 | assert.NoError(t, err, "partial read of 2") 57 | assert.Equal(t, len(b2), n, "number of bytes in partial read of 2") 58 | 59 | assert.Equal(t, b[1:], b2) 60 | } 61 | 62 | func TestReadAfterClose(t *testing.T) { 63 | pipe := NewStreamBufferedPipe() 64 | b := []byte{0x01, 0x02, 0x03} 65 | pipe.Write(b) 66 | b2 := make([]byte, len(b)) 67 | pipe.Close() 68 | n, err := pipe.Read(b2) 69 | assert.NoError(t, err, "simple read") 70 | assert.Equal(t, len(b), n, "number of bytes read") 71 | 72 | assert.Equal(t, b, b2) 73 | } 74 | 75 | func BenchmarkBufferedPipe_RW(b *testing.B) { 76 | const PAYLOAD_LEN = 1000 77 | testData := make([]byte, PAYLOAD_LEN) 78 | rand.Read(testData) 79 | 80 | pipe := NewStreamBufferedPipe() 81 | 82 | smallBuf := make([]byte, PAYLOAD_LEN-10) 83 | go func() { 84 | for { 85 | pipe.Read(smallBuf) 86 | } 87 | }() 88 | b.SetBytes(int64(len(testData))) 89 | b.ResetTimer() 90 | for i := 0; i < b.N; i++ { 91 | pipe.Write(testData) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /internal/multiplex/streamBufferedPipe.go: -------------------------------------------------------------------------------- 1 | // This is base on https://github.com/golang/go/blob/0436b162397018c45068b47ca1b5924a3eafdee0/src/net/net_fake.go#L173 2 | 3 | package multiplex 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // The point of a streamBufferedPipe is that Read() will block until data is available 13 | type streamBufferedPipe struct { 14 | buf *bytes.Buffer 15 | 16 | closed bool 17 | rwCond *sync.Cond 18 | rDeadline time.Time 19 | wtTimeout time.Duration 20 | 21 | timeoutTimer *time.Timer 22 | } 23 | 24 | func NewStreamBufferedPipe() *streamBufferedPipe { 25 | p := &streamBufferedPipe{ 26 | rwCond: sync.NewCond(&sync.Mutex{}), 27 | buf: new(bytes.Buffer), 28 | } 29 | return p 30 | } 31 | 32 | func (p *streamBufferedPipe) Read(target []byte) (int, error) { 33 | p.rwCond.L.Lock() 34 | defer p.rwCond.L.Unlock() 35 | for { 36 | if p.closed && p.buf.Len() == 0 { 37 | return 0, io.EOF 38 | } 39 | 40 | hasRDeadline := !p.rDeadline.IsZero() 41 | if hasRDeadline { 42 | if time.Until(p.rDeadline) <= 0 { 43 | return 0, ErrTimeout 44 | } 45 | } 46 | if p.buf.Len() > 0 { 47 | break 48 | } 49 | 50 | if hasRDeadline { 51 | p.broadcastAfter(time.Until(p.rDeadline)) 52 | } 53 | p.rwCond.Wait() 54 | } 55 | n, err := p.buf.Read(target) 56 | // err will always be nil because we have already verified that buf.Len() != 0 57 | p.rwCond.Broadcast() 58 | return n, err 59 | } 60 | 61 | func (p *streamBufferedPipe) Write(input []byte) (int, error) { 62 | p.rwCond.L.Lock() 63 | defer p.rwCond.L.Unlock() 64 | for { 65 | if p.closed { 66 | return 0, io.ErrClosedPipe 67 | } 68 | if p.buf.Len() <= recvBufferSizeLimit { 69 | // if p.buf gets too large, write() will panic. We don't want this to happen 70 | break 71 | } 72 | p.rwCond.Wait() 73 | } 74 | n, err := p.buf.Write(input) 75 | // err will always be nil 76 | p.rwCond.Broadcast() 77 | return n, err 78 | } 79 | 80 | func (p *streamBufferedPipe) Close() error { 81 | p.rwCond.L.Lock() 82 | defer p.rwCond.L.Unlock() 83 | 84 | p.closed = true 85 | p.rwCond.Broadcast() 86 | return nil 87 | } 88 | 89 | func (p *streamBufferedPipe) SetReadDeadline(t time.Time) { 90 | p.rwCond.L.Lock() 91 | defer p.rwCond.L.Unlock() 92 | 93 | p.rDeadline = t 94 | p.rwCond.Broadcast() 95 | } 96 | 97 | func (p *streamBufferedPipe) broadcastAfter(d time.Duration) { 98 | if p.timeoutTimer != nil { 99 | p.timeoutTimer.Stop() 100 | } 101 | p.timeoutTimer = time.AfterFunc(d, p.rwCond.Broadcast) 102 | } 103 | -------------------------------------------------------------------------------- /internal/common/copy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009 The Go Authors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | /* 31 | Forked from https://golang.org/src/io/io.go 32 | */ 33 | package common 34 | 35 | import ( 36 | "io" 37 | "net" 38 | ) 39 | 40 | func Copy(dst net.Conn, src net.Conn) (written int64, err error) { 41 | defer func() { src.Close(); dst.Close() }() 42 | 43 | // If the reader has a WriteTo method, use it to do the copy. 44 | // Avoids an allocation and a copy. 45 | if wt, ok := src.(io.WriterTo); ok { 46 | return wt.WriteTo(dst) 47 | } 48 | // Similarly, if the writer has a ReadFrom method, use it to do the copy. 49 | if rt, ok := dst.(io.ReaderFrom); ok { 50 | return rt.ReadFrom(src) 51 | } 52 | 53 | size := 32 * 1024 54 | buf := make([]byte, size) 55 | for { 56 | nr, er := src.Read(buf) 57 | if nr > 0 { 58 | nw, ew := dst.Write(buf[0:nr]) 59 | if nw > 0 { 60 | written += int64(nw) 61 | } 62 | if ew != nil { 63 | err = ew 64 | break 65 | } 66 | if nr != nw { 67 | err = io.ErrShortWrite 68 | break 69 | } 70 | } 71 | if er != nil { 72 | if er != io.EOF { 73 | err = er 74 | } 75 | break 76 | } 77 | } 78 | return written, err 79 | } 80 | -------------------------------------------------------------------------------- /internal/multiplex/streamBuffer_test.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | //"log" 8 | "sort" 9 | "testing" 10 | ) 11 | 12 | func TestRecvNewFrame(t *testing.T) { 13 | inOrder := []uint64{5, 6, 7, 8, 9, 10, 11} 14 | outOfOrder0 := []uint64{5, 7, 8, 6, 11, 10, 9} 15 | outOfOrder1 := []uint64{1, 96, 47, 2, 29, 18, 60, 8, 74, 22, 82, 58, 44, 51, 57, 71, 90, 94, 68, 83, 61, 91, 39, 97, 85, 63, 46, 73, 54, 84, 76, 98, 93, 79, 75, 50, 67, 37, 92, 99, 42, 77, 17, 16, 38, 3, 100, 24, 31, 7, 36, 40, 86, 64, 34, 45, 12, 5, 9, 27, 21, 26, 35, 6, 65, 69, 53, 4, 48, 28, 30, 56, 32, 11, 80, 66, 25, 41, 78, 13, 88, 62, 15, 70, 49, 43, 72, 23, 10, 55, 52, 95, 14, 59, 87, 33, 19, 20, 81, 89} 16 | outOfOrder2 := []uint64{1<<32 - 5, 1<<32 + 3, 1 << 32, 1<<32 - 3, 1<<32 - 4, 1<<32 + 2, 1<<32 - 2, 1<<32 - 1, 1<<32 + 1} 17 | 18 | test := func(set []uint64, ct *testing.T) { 19 | sb := NewStreamBuffer() 20 | sb.nextRecvSeq = set[0] 21 | for _, n := range set { 22 | bu64 := make([]byte, 8) 23 | binary.BigEndian.PutUint64(bu64, n) 24 | sb.Write(&Frame{ 25 | Seq: n, 26 | Payload: bu64, 27 | }) 28 | } 29 | 30 | var sortedResult []uint64 31 | for x := 0; x < len(set); x++ { 32 | oct := make([]byte, 8) 33 | n, err := sb.Read(oct) 34 | if n != 8 || err != nil { 35 | ct.Error("failed to read from sorted Buf", n, err) 36 | return 37 | } 38 | //log.Print(p) 39 | sortedResult = append(sortedResult, binary.BigEndian.Uint64(oct)) 40 | } 41 | targetSorted := make([]uint64, len(set)) 42 | copy(targetSorted, set) 43 | sort.Slice(targetSorted, func(i, j int) bool { return targetSorted[i] < targetSorted[j] }) 44 | 45 | for i := range targetSorted { 46 | if sortedResult[i] != targetSorted[i] { 47 | goto fail 48 | } 49 | } 50 | sb.Close() 51 | return 52 | fail: 53 | ct.Error( 54 | "expecting", targetSorted, 55 | "got", sortedResult, 56 | ) 57 | } 58 | 59 | t.Run("in order", func(t *testing.T) { 60 | test(inOrder, t) 61 | }) 62 | t.Run("out of order0", func(t *testing.T) { 63 | test(outOfOrder0, t) 64 | }) 65 | t.Run("out of order1", func(t *testing.T) { 66 | test(outOfOrder1, t) 67 | }) 68 | t.Run("out of order wrap", func(t *testing.T) { 69 | test(outOfOrder2, t) 70 | }) 71 | } 72 | 73 | func TestStreamBuffer_RecvThenClose(t *testing.T) { 74 | const testDataLen = 128 75 | sb := NewStreamBuffer() 76 | testData := make([]byte, testDataLen) 77 | testFrame := Frame{ 78 | StreamID: 0, 79 | Seq: 0, 80 | Closing: 0, 81 | Payload: testData, 82 | } 83 | sb.Write(&testFrame) 84 | sb.Close() 85 | 86 | readBuf := make([]byte, testDataLen) 87 | _, err := io.ReadFull(sb, readBuf) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/ecdh/curve25519.go: -------------------------------------------------------------------------------- 1 | // This code is forked from https://github.com/wsddn/go-ecdh/blob/master/curve25519.go 2 | /* 3 | Copyright (c) 2014, tang0th 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of tang0th nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package ecdh 30 | 31 | import ( 32 | "crypto" 33 | "io" 34 | 35 | "golang.org/x/crypto/curve25519" 36 | ) 37 | 38 | func GenerateKey(rand io.Reader) (crypto.PrivateKey, crypto.PublicKey, error) { 39 | var pub, priv [32]byte 40 | var err error 41 | 42 | _, err = io.ReadFull(rand, priv[:]) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | priv[0] &= 248 48 | priv[31] &= 127 49 | priv[31] |= 64 50 | 51 | curve25519.ScalarBaseMult(&pub, &priv) 52 | 53 | return &priv, &pub, nil 54 | } 55 | 56 | func Marshal(p crypto.PublicKey) []byte { 57 | pub := p.(*[32]byte) 58 | return pub[:] 59 | } 60 | 61 | func Unmarshal(data []byte) (crypto.PublicKey, bool) { 62 | var pub [32]byte 63 | if len(data) != 32 { 64 | return nil, false 65 | } 66 | 67 | copy(pub[:], data) 68 | return &pub, true 69 | } 70 | 71 | func GenerateSharedSecret(privKey crypto.PrivateKey, pubKey crypto.PublicKey) ([]byte, error) { 72 | var priv, pub *[32]byte 73 | 74 | priv = privKey.(*[32]byte) 75 | pub = pubKey.(*[32]byte) 76 | 77 | return curve25519.X25519(priv[:], pub[:]) 78 | } 79 | -------------------------------------------------------------------------------- /internal/client/auth_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | "time" 7 | 8 | "github.com/cbeuw/Cloak/internal/common" 9 | "github.com/cbeuw/Cloak/internal/multiplex" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMakeAuthenticationPayload(t *testing.T) { 14 | tests := []struct { 15 | authInfo AuthInfo 16 | expPayload authenticationPayload 17 | expSecret [32]byte 18 | }{ 19 | { 20 | AuthInfo{ 21 | Unordered: false, 22 | SessionId: 3421516597, 23 | UID: []byte{ 24 | 0x4c, 0xd8, 0xcc, 0x15, 0x60, 0x0d, 0x7e, 25 | 0xb6, 0x81, 0x31, 0xfd, 0x80, 0x97, 0x67, 0x37, 0x46}, 26 | ServerPubKey: &[32]byte{ 27 | 0x21, 0x8a, 0x14, 0xce, 0x49, 0x5e, 0xfd, 0x3f, 28 | 0xe4, 0xae, 0x21, 0x3e, 0x51, 0xf7, 0x66, 0xec, 29 | 0x01, 0xd0, 0xb4, 0x87, 0x86, 0x9c, 0x15, 0x9b, 30 | 0x86, 0x19, 0x53, 0x6e, 0x60, 0xe9, 0x51, 0x42}, 31 | ProxyMethod: "shadowsocks", 32 | EncryptionMethod: multiplex.EncryptionMethodPlain, 33 | MockDomain: "d2jkinvisak5y9.cloudfront.net", 34 | WorldState: common.WorldState{ 35 | Rand: bytes.NewBuffer([]byte{ 36 | 0xf1, 0x1e, 0x42, 0xe1, 0x84, 0x22, 0x07, 0xc5, 37 | 0xc3, 0x5c, 0x0f, 0x7b, 0x01, 0xf3, 0x65, 0x2d, 38 | 0xd7, 0x9b, 0xad, 0xb0, 0xb2, 0x77, 0xa2, 0x06, 39 | 0x6b, 0x78, 0x1b, 0x74, 0x1f, 0x43, 0xc9, 0x80}), 40 | Now: func() time.Time { return time.Unix(1579908372, 0) }, 41 | }, 42 | }, 43 | authenticationPayload{ 44 | randPubKey: [32]byte{ 45 | 0xee, 0x9e, 0x41, 0x4e, 0xb3, 0x3b, 0x85, 0x03, 46 | 0x6d, 0x85, 0xba, 0x30, 0x11, 0x31, 0x10, 0x24, 47 | 0x4f, 0x7b, 0xd5, 0x38, 0x50, 0x0f, 0xf2, 0x4d, 48 | 0xa3, 0xdf, 0xba, 0x76, 0x0a, 0xe9, 0x19, 0x19}, 49 | ciphertextWithTag: [64]byte{ 50 | 0x71, 0xb1, 0x6c, 0x5a, 0x60, 0x46, 0x90, 0x12, 51 | 0x36, 0x3b, 0x1b, 0xc4, 0x79, 0x3c, 0xab, 0xdd, 52 | 0x5a, 0x53, 0xc5, 0xed, 0xaf, 0xdb, 0x10, 0x98, 53 | 0x83, 0x96, 0x81, 0xa6, 0xfc, 0xa2, 0x1e, 0xb0, 54 | 0x89, 0xb2, 0x29, 0x71, 0x7e, 0x45, 0x97, 0x54, 55 | 0x11, 0x7d, 0x9b, 0x92, 0xbb, 0xd6, 0xce, 0x37, 56 | 0x3b, 0xb8, 0x8b, 0xfb, 0xb6, 0x40, 0xf0, 0x2c, 57 | 0x6c, 0x55, 0xb9, 0xfc, 0x5d, 0x34, 0x89, 0x41}, 58 | }, 59 | [32]byte{ 60 | 0xc7, 0xc6, 0x9b, 0xbe, 0xec, 0xf8, 0x35, 0x55, 61 | 0x67, 0x20, 0xcd, 0xeb, 0x74, 0x16, 0xc5, 0x60, 62 | 0xee, 0x9d, 0x63, 0x1a, 0x44, 0xc5, 0x09, 0xf6, 63 | 0xe0, 0x24, 0xad, 0xd2, 0x10, 0xe3, 0x4a, 0x11}, 64 | }, 65 | } 66 | for _, tc := range tests { 67 | func() { 68 | payload, sharedSecret := makeAuthenticationPayload(tc.authInfo) 69 | assert.Equal(t, tc.expPayload, payload, "payload doesn't match") 70 | assert.Equal(t, tc.expSecret, sharedSecret, "shared secret doesn't match") 71 | }() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/client/connector.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/cbeuw/Cloak/internal/common" 10 | 11 | mux "github.com/cbeuw/Cloak/internal/multiplex" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // On different invocations to MakeSession, authInfo.SessionId MUST be different 16 | func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.Dialer) *mux.Session { 17 | log.Info("Attempting to start a new session") 18 | 19 | connsCh := make(chan net.Conn, connConfig.NumConn) 20 | var _sessionKey atomic.Value 21 | var wg sync.WaitGroup 22 | for i := 0; i < connConfig.NumConn; i++ { 23 | wg.Add(1) 24 | transportConfig := connConfig.Transport 25 | go func() { 26 | makeconn: 27 | transportConn := transportConfig.CreateTransport() 28 | remoteConn, err := dialer.Dial("tcp", connConfig.RemoteAddr) 29 | if err != nil { 30 | log.Errorf("Failed to establish new connections to remote: %v", err) 31 | // TODO increase the interval if failed multiple times 32 | time.Sleep(time.Second * 3) 33 | goto makeconn 34 | } 35 | 36 | sk, err := transportConn.Handshake(remoteConn, authInfo) 37 | if err != nil { 38 | log.Errorf("Failed to prepare connection to remote: %v", err) 39 | transportConn.Close() 40 | 41 | // In Cloak v2.11.0, we've updated uTLS version and subsequently increased the first packet size for chrome above 1500 42 | // https://github.com/cbeuw/Cloak/pull/306#issuecomment-2862728738. As a backwards compatibility feature, if we fail 43 | // to connect using chrome signature, retry with firefox which has a smaller packet size. 44 | if transportConfig.mode == "direct" && transportConfig.browser == chrome { 45 | transportConfig.browser = firefox 46 | log.Warnf("failed to connect with chrome signature, falling back to retry with firefox") 47 | } 48 | time.Sleep(time.Second * 3) 49 | 50 | goto makeconn 51 | } 52 | // sessionKey given by each connection should be identical 53 | _sessionKey.Store(sk) 54 | connsCh <- transportConn 55 | wg.Done() 56 | }() 57 | } 58 | wg.Wait() 59 | log.Debug("All underlying connections established") 60 | 61 | sessionKey := _sessionKey.Load().([32]byte) 62 | obfuscator, err := mux.MakeObfuscator(authInfo.EncryptionMethod, sessionKey) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | seshConfig := mux.SessionConfig{ 68 | Singleplex: connConfig.Singleplex, 69 | Obfuscator: obfuscator, 70 | Valve: nil, 71 | Unordered: authInfo.Unordered, 72 | MsgOnWireSizeLimit: appDataMaxLength, 73 | } 74 | sesh := mux.MakeSession(authInfo.SessionId, seshConfig) 75 | 76 | for i := 0; i < connConfig.NumConn; i++ { 77 | conn := <-connsCh 78 | sesh.AddConnection(conn) 79 | } 80 | 81 | log.Infof("Session %v established", authInfo.SessionId) 82 | return sesh 83 | } 84 | -------------------------------------------------------------------------------- /internal/server/auth.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/cbeuw/Cloak/internal/common" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type ClientInfo struct { 16 | UID []byte 17 | SessionId uint32 18 | ProxyMethod string 19 | EncryptionMethod byte 20 | Unordered bool 21 | Transport Transport 22 | } 23 | 24 | type authFragments struct { 25 | sharedSecret [32]byte 26 | randPubKey [32]byte 27 | ciphertextWithTag [64]byte 28 | } 29 | 30 | const ( 31 | UNORDERED_FLAG = 0x01 // 0000 0001 32 | ) 33 | 34 | var ErrTimestampOutOfWindow = errors.New("timestamp is outside of the accepting window") 35 | 36 | // decryptClientInfo checks if a the authFragments are valid. It doesn't check if the UID is authorised 37 | func decryptClientInfo(fragments authFragments, serverTime time.Time) (info ClientInfo, err error) { 38 | var plaintext []byte 39 | plaintext, err = common.AESGCMDecrypt(fragments.randPubKey[0:12], fragments.sharedSecret[:], fragments.ciphertextWithTag[:]) 40 | if err != nil { 41 | return 42 | } 43 | 44 | info = ClientInfo{ 45 | UID: plaintext[0:16], 46 | SessionId: 0, 47 | ProxyMethod: string(bytes.Trim(plaintext[16:28], "\x00")), 48 | EncryptionMethod: plaintext[28], 49 | Unordered: plaintext[41]&UNORDERED_FLAG != 0, 50 | } 51 | 52 | timestamp := int64(binary.BigEndian.Uint64(plaintext[29:37])) 53 | clientTime := time.Unix(timestamp, 0) 54 | if !(clientTime.After(serverTime.Add(-timestampTolerance)) && clientTime.Before(serverTime.Add(timestampTolerance))) { 55 | err = fmt.Errorf("%v: received timestamp %v", ErrTimestampOutOfWindow, timestamp) 56 | return 57 | } 58 | info.SessionId = binary.BigEndian.Uint32(plaintext[37:41]) 59 | return 60 | } 61 | 62 | var ErrReplay = errors.New("duplicate random") 63 | var ErrBadProxyMethod = errors.New("invalid proxy method") 64 | var ErrBadDecryption = errors.New("decryption/authentication failure") 65 | 66 | // AuthFirstPacket checks if the first packet of data is ClientHello or HTTP GET, and checks if it was from a Cloak client 67 | // if it is from a Cloak client, it returns the ClientInfo with the decrypted fields. It doesn't check if the user 68 | // is authorised. It also returns a finisher callback function to be called when the caller wishes to proceed with 69 | // the handshake 70 | func AuthFirstPacket(firstPacket []byte, transport Transport, sta *State) (info ClientInfo, finisher Responder, err error) { 71 | fragments, finisher, err := transport.processFirstPacket(firstPacket, sta.StaticPv) 72 | if err != nil { 73 | return 74 | } 75 | 76 | if sta.registerRandom(fragments.randPubKey) { 77 | err = ErrReplay 78 | return 79 | } 80 | 81 | info, err = decryptClientInfo(fragments, sta.WorldState.Now().UTC()) 82 | if err != nil { 83 | log.Debug(err) 84 | err = fmt.Errorf("%w: %v", ErrBadDecryption, err) 85 | return 86 | } 87 | info.Transport = transport 88 | return 89 | } 90 | -------------------------------------------------------------------------------- /internal/common/tls.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "net" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | const ( 13 | VersionTLS11 = 0x0301 14 | VersionTLS13 = 0x0303 15 | 16 | recordLayerLength = 5 17 | 18 | Handshake = 22 19 | ApplicationData = 23 20 | 21 | initialWriteBufSize = 14336 22 | ) 23 | 24 | func AddRecordLayer(input []byte, typ byte, ver uint16) []byte { 25 | msgLen := len(input) 26 | retLen := msgLen + recordLayerLength 27 | var ret []byte 28 | ret = make([]byte, retLen) 29 | copy(ret[recordLayerLength:], input) 30 | ret[0] = typ 31 | ret[1] = byte(ver >> 8) 32 | ret[2] = byte(ver) 33 | ret[3] = byte(msgLen >> 8) 34 | ret[4] = byte(msgLen) 35 | return ret 36 | } 37 | 38 | type TLSConn struct { 39 | net.Conn 40 | writeBufPool sync.Pool 41 | } 42 | 43 | func NewTLSConn(conn net.Conn) *TLSConn { 44 | return &TLSConn{ 45 | Conn: conn, 46 | writeBufPool: sync.Pool{New: func() interface{} { 47 | b := make([]byte, 0, initialWriteBufSize) 48 | b = append(b, ApplicationData, byte(VersionTLS13>>8), byte(VersionTLS13&0xFF)) 49 | return &b 50 | }}, 51 | } 52 | } 53 | 54 | func (tls *TLSConn) LocalAddr() net.Addr { 55 | return tls.Conn.LocalAddr() 56 | } 57 | 58 | func (tls *TLSConn) RemoteAddr() net.Addr { 59 | return tls.Conn.RemoteAddr() 60 | } 61 | 62 | func (tls *TLSConn) SetDeadline(t time.Time) error { 63 | return tls.Conn.SetDeadline(t) 64 | } 65 | 66 | func (tls *TLSConn) SetReadDeadline(t time.Time) error { 67 | return tls.Conn.SetReadDeadline(t) 68 | } 69 | 70 | func (tls *TLSConn) SetWriteDeadline(t time.Time) error { 71 | return tls.Conn.SetWriteDeadline(t) 72 | } 73 | 74 | func (tls *TLSConn) Read(buffer []byte) (n int, err error) { 75 | // TCP is a stream. Multiple TLS messages can arrive at the same time, 76 | // a single message can also be segmented due to MTU of the IP layer. 77 | // This function guareentees a single TLS message to be read and everything 78 | // else is left in the buffer. 79 | if len(buffer) < recordLayerLength { 80 | return 0, io.ErrShortBuffer 81 | } 82 | _, err = io.ReadFull(tls.Conn, buffer[:recordLayerLength]) 83 | if err != nil { 84 | return 85 | } 86 | 87 | dataLength := int(binary.BigEndian.Uint16(buffer[3:5])) 88 | if dataLength > len(buffer) { 89 | err = io.ErrShortBuffer 90 | return 91 | } 92 | // we overwrite the record layer here 93 | return io.ReadFull(tls.Conn, buffer[:dataLength]) 94 | } 95 | 96 | func (tls *TLSConn) Write(in []byte) (n int, err error) { 97 | msgLen := len(in) 98 | if msgLen > 1<<14+256 { // https://tools.ietf.org/html/rfc8446#section-5.2 99 | return 0, errors.New("message is too long") 100 | } 101 | writeBuf := tls.writeBufPool.Get().(*[]byte) 102 | *writeBuf = append(*writeBuf, byte(msgLen>>8), byte(msgLen&0xFF)) 103 | *writeBuf = append(*writeBuf, in...) 104 | n, err = tls.Conn.Write(*writeBuf) 105 | *writeBuf = (*writeBuf)[:3] 106 | tls.writeBufPool.Put(writeBuf) 107 | return n - recordLayerLength, err 108 | } 109 | 110 | func (tls *TLSConn) Close() error { 111 | return tls.Conn.Close() 112 | } 113 | -------------------------------------------------------------------------------- /internal/multiplex/datagramBufferedPipe.go: -------------------------------------------------------------------------------- 1 | // This is base on https://github.com/golang/go/blob/0436b162397018c45068b47ca1b5924a3eafdee0/src/net/net_fake.go#L173 2 | 3 | package multiplex 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // datagramBufferedPipe is the same as streamBufferedPipe with the exception that it's message-oriented, 13 | // instead of byte-oriented. The integrity of datagrams written into this buffer is preserved. 14 | // it won't get chopped up into individual bytes 15 | type datagramBufferedPipe struct { 16 | pLens []int 17 | buf *bytes.Buffer 18 | closed bool 19 | rwCond *sync.Cond 20 | wtTimeout time.Duration 21 | rDeadline time.Time 22 | 23 | timeoutTimer *time.Timer 24 | } 25 | 26 | func NewDatagramBufferedPipe() *datagramBufferedPipe { 27 | d := &datagramBufferedPipe{ 28 | rwCond: sync.NewCond(&sync.Mutex{}), 29 | buf: new(bytes.Buffer), 30 | } 31 | return d 32 | } 33 | 34 | func (d *datagramBufferedPipe) Read(target []byte) (int, error) { 35 | d.rwCond.L.Lock() 36 | defer d.rwCond.L.Unlock() 37 | for { 38 | if d.closed && len(d.pLens) == 0 { 39 | return 0, io.EOF 40 | } 41 | 42 | hasRDeadline := !d.rDeadline.IsZero() 43 | if hasRDeadline { 44 | if time.Until(d.rDeadline) <= 0 { 45 | return 0, ErrTimeout 46 | } 47 | } 48 | 49 | if len(d.pLens) > 0 { 50 | break 51 | } 52 | 53 | if hasRDeadline { 54 | d.broadcastAfter(time.Until(d.rDeadline)) 55 | } 56 | d.rwCond.Wait() 57 | } 58 | dataLen := d.pLens[0] 59 | if len(target) < dataLen { 60 | return 0, io.ErrShortBuffer 61 | } 62 | d.pLens = d.pLens[1:] 63 | d.buf.Read(target[:dataLen]) 64 | // err will always be nil because we have already verified that buf.Len() != 0 65 | d.rwCond.Broadcast() 66 | return dataLen, nil 67 | } 68 | 69 | func (d *datagramBufferedPipe) Write(f *Frame) (toBeClosed bool, err error) { 70 | d.rwCond.L.Lock() 71 | defer d.rwCond.L.Unlock() 72 | for { 73 | if d.closed { 74 | return true, io.ErrClosedPipe 75 | } 76 | if d.buf.Len() <= recvBufferSizeLimit { 77 | // if d.buf gets too large, write() will panic. We don't want this to happen 78 | break 79 | } 80 | d.rwCond.Wait() 81 | } 82 | 83 | if f.Closing != closingNothing { 84 | d.closed = true 85 | d.rwCond.Broadcast() 86 | return true, nil 87 | } 88 | 89 | dataLen := len(f.Payload) 90 | d.pLens = append(d.pLens, dataLen) 91 | d.buf.Write(f.Payload) 92 | // err will always be nil 93 | d.rwCond.Broadcast() 94 | return false, nil 95 | } 96 | 97 | func (d *datagramBufferedPipe) Close() error { 98 | d.rwCond.L.Lock() 99 | defer d.rwCond.L.Unlock() 100 | 101 | d.closed = true 102 | d.rwCond.Broadcast() 103 | return nil 104 | } 105 | 106 | func (d *datagramBufferedPipe) SetReadDeadline(t time.Time) { 107 | d.rwCond.L.Lock() 108 | defer d.rwCond.L.Unlock() 109 | 110 | d.rDeadline = t 111 | d.rwCond.Broadcast() 112 | } 113 | 114 | func (d *datagramBufferedPipe) broadcastAfter(t time.Duration) { 115 | if d.timeoutTimer != nil { 116 | d.timeoutTimer.Stop() 117 | } 118 | d.timeoutTimer = time.AfterFunc(t, d.rwCond.Broadcast) 119 | } 120 | -------------------------------------------------------------------------------- /internal/server/websocket.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto" 7 | "encoding/base64" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | "net/http" 13 | 14 | "github.com/cbeuw/Cloak/internal/common" 15 | "github.com/cbeuw/Cloak/internal/ecdh" 16 | ) 17 | 18 | type WebSocket struct{} 19 | 20 | func (WebSocket) String() string { return "WebSocket" } 21 | 22 | func (WebSocket) processFirstPacket(reqPacket []byte, privateKey crypto.PrivateKey) (fragments authFragments, respond Responder, err error) { 23 | var req *http.Request 24 | req, err = http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqPacket))) 25 | if err != nil { 26 | err = fmt.Errorf("failed to parse first HTTP GET: %v", err) 27 | return 28 | } 29 | var hiddenData []byte 30 | hiddenData, err = base64.StdEncoding.DecodeString(req.Header.Get("hidden")) 31 | 32 | fragments, err = WebSocket{}.unmarshalHidden(hiddenData, privateKey) 33 | if err != nil { 34 | err = fmt.Errorf("failed to unmarshal hidden data from WS into authFragments: %v", err) 35 | return 36 | } 37 | 38 | respond = WebSocket{}.makeResponder(reqPacket, fragments.sharedSecret) 39 | 40 | return 41 | } 42 | 43 | func (WebSocket) makeResponder(reqPacket []byte, sharedSecret [32]byte) Responder { 44 | respond := func(originalConn net.Conn, sessionKey [32]byte, randSource io.Reader) (preparedConn net.Conn, err error) { 45 | handler := newWsHandshakeHandler() 46 | 47 | // For an explanation of the following 3 lines, see the comments in websocketAux.go 48 | http.Serve(newWsAcceptor(originalConn, reqPacket), handler) 49 | 50 | <-handler.finished 51 | preparedConn = handler.conn 52 | nonce := make([]byte, 12) 53 | common.RandRead(randSource, nonce) 54 | 55 | // reply: [12 bytes nonce][32 bytes encrypted session key][16 bytes authentication tag] 56 | encryptedKey, err := common.AESGCMEncrypt(nonce, sharedSecret[:], sessionKey[:]) // 32 + 16 = 48 bytes 57 | if err != nil { 58 | err = fmt.Errorf("failed to encrypt reply: %v", err) 59 | return 60 | } 61 | reply := append(nonce, encryptedKey...) 62 | _, err = preparedConn.Write(reply) 63 | if err != nil { 64 | err = fmt.Errorf("failed to write reply: %v", err) 65 | preparedConn.Close() 66 | return 67 | } 68 | return 69 | } 70 | return respond 71 | } 72 | 73 | var ErrBadGET = errors.New("non (or malformed) HTTP GET") 74 | 75 | func (WebSocket) unmarshalHidden(hidden []byte, staticPv crypto.PrivateKey) (fragments authFragments, err error) { 76 | if len(hidden) < 96 { 77 | err = ErrBadGET 78 | return 79 | } 80 | 81 | copy(fragments.randPubKey[:], hidden[0:32]) 82 | ephPub, ok := ecdh.Unmarshal(fragments.randPubKey[:]) 83 | if !ok { 84 | err = ErrInvalidPubKey 85 | return 86 | } 87 | 88 | var sharedSecret []byte 89 | sharedSecret, err = ecdh.GenerateSharedSecret(staticPv, ephPub) 90 | if err != nil { 91 | return 92 | } 93 | 94 | copy(fragments.sharedSecret[:], sharedSecret) 95 | 96 | if len(hidden[32:]) != 64 { 97 | err = fmt.Errorf("%v: %v", ErrCiphertextLength, len(hidden[32:])) 98 | return 99 | } 100 | 101 | copy(fragments.ciphertextWithTag[:], hidden[32:]) 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /internal/server/TLS.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | 10 | "github.com/cbeuw/Cloak/internal/common" 11 | "github.com/cbeuw/Cloak/internal/ecdh" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | const appDataMaxLength = 16401 17 | 18 | type TLS struct{} 19 | 20 | var ErrBadClientHello = errors.New("non (or malformed) ClientHello") 21 | 22 | func (TLS) String() string { return "TLS" } 23 | 24 | func (TLS) processFirstPacket(clientHello []byte, privateKey crypto.PrivateKey) (fragments authFragments, respond Responder, err error) { 25 | ch, err := parseClientHello(clientHello) 26 | if err != nil { 27 | log.Debug(err) 28 | err = ErrBadClientHello 29 | return 30 | } 31 | 32 | fragments, err = TLS{}.unmarshalClientHello(ch, privateKey) 33 | if err != nil { 34 | err = fmt.Errorf("failed to unmarshal ClientHello into authFragments: %v", err) 35 | return 36 | } 37 | 38 | respond = TLS{}.makeResponder(ch.sessionId, fragments.sharedSecret) 39 | 40 | return 41 | } 42 | 43 | func (TLS) makeResponder(clientHelloSessionId []byte, sharedSecret [32]byte) Responder { 44 | respond := func(originalConn net.Conn, sessionKey [32]byte, randSource io.Reader) (preparedConn net.Conn, err error) { 45 | // the cert length needs to be the same for all handshakes belonging to the same session 46 | // we can use sessionKey as a seed here to ensure consistency 47 | possibleCertLengths := []int{42, 27, 68, 59, 36, 44, 46} 48 | cert := make([]byte, possibleCertLengths[common.RandInt(len(possibleCertLengths))]) 49 | common.RandRead(randSource, cert) 50 | 51 | var nonce [12]byte 52 | common.RandRead(randSource, nonce[:]) 53 | encryptedSessionKey, err := common.AESGCMEncrypt(nonce[:], sharedSecret[:], sessionKey[:]) 54 | if err != nil { 55 | return 56 | } 57 | var encryptedSessionKeyArr [48]byte 58 | copy(encryptedSessionKeyArr[:], encryptedSessionKey) 59 | 60 | reply := composeReply(clientHelloSessionId, nonce, encryptedSessionKeyArr, cert) 61 | _, err = originalConn.Write(reply) 62 | if err != nil { 63 | err = fmt.Errorf("failed to write TLS reply: %v", err) 64 | originalConn.Close() 65 | return 66 | } 67 | preparedConn = common.NewTLSConn(originalConn) 68 | return 69 | } 70 | return respond 71 | } 72 | 73 | func (TLS) unmarshalClientHello(ch *ClientHello, staticPv crypto.PrivateKey) (fragments authFragments, err error) { 74 | copy(fragments.randPubKey[:], ch.random) 75 | ephPub, ok := ecdh.Unmarshal(fragments.randPubKey[:]) 76 | if !ok { 77 | err = ErrInvalidPubKey 78 | return 79 | } 80 | 81 | var sharedSecret []byte 82 | sharedSecret, err = ecdh.GenerateSharedSecret(staticPv, ephPub) 83 | if err != nil { 84 | return 85 | } 86 | 87 | copy(fragments.sharedSecret[:], sharedSecret) 88 | var keyShare []byte 89 | keyShare, err = parseKeyShare(ch.extensions[[2]byte{0x00, 0x33}]) 90 | if err != nil { 91 | return 92 | } 93 | 94 | ctxTag := append(ch.sessionId, keyShare...) 95 | if len(ctxTag) != 64 { 96 | err = fmt.Errorf("%v: %v", ErrCiphertextLength, len(ctxTag)) 97 | return 98 | } 99 | copy(fragments.ciphertextWithTag[:], ctxTag) 100 | return 101 | } 102 | -------------------------------------------------------------------------------- /internal/common/crypto_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "errors" 7 | "io" 8 | "math/rand" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | const gcmTagSize = 16 15 | 16 | func TestAESGCM(t *testing.T) { 17 | // test vectors from https://luca-giuzzi.unibs.it/corsi/Support/papers-cryptography/gcm-spec.pdf 18 | t.Run("correct 128", func(t *testing.T) { 19 | key, _ := hex.DecodeString("00000000000000000000000000000000") 20 | plaintext, _ := hex.DecodeString("") 21 | nonce, _ := hex.DecodeString("000000000000000000000000") 22 | ciphertext, _ := hex.DecodeString("") 23 | tag, _ := hex.DecodeString("58e2fccefa7e3061367f1d57a4e7455a") 24 | 25 | encryptedWithTag, err := AESGCMEncrypt(nonce, key, plaintext) 26 | assert.NoError(t, err) 27 | assert.Equal(t, ciphertext, encryptedWithTag[:len(plaintext)]) 28 | assert.Equal(t, tag, encryptedWithTag[len(plaintext):len(plaintext)+gcmTagSize]) 29 | 30 | decrypted, err := AESGCMDecrypt(nonce, key, encryptedWithTag) 31 | assert.NoError(t, err) 32 | // slight inconvenience here that assert.Equal does not consider a nil slice and an empty slice to be 33 | // equal. decrypted should be []byte(nil) but plaintext is []byte{} 34 | assert.True(t, bytes.Equal(plaintext, decrypted)) 35 | }) 36 | t.Run("bad key size", func(t *testing.T) { 37 | key, _ := hex.DecodeString("0000000000000000000000000000") 38 | plaintext, _ := hex.DecodeString("") 39 | nonce, _ := hex.DecodeString("000000000000000000000000") 40 | ciphertext, _ := hex.DecodeString("") 41 | tag, _ := hex.DecodeString("58e2fccefa7e3061367f1d57a4e7455a") 42 | 43 | _, err := AESGCMEncrypt(nonce, key, plaintext) 44 | assert.Error(t, err) 45 | 46 | _, err = AESGCMDecrypt(nonce, key, append(ciphertext, tag...)) 47 | assert.Error(t, err) 48 | }) 49 | t.Run("bad nonce size", func(t *testing.T) { 50 | key, _ := hex.DecodeString("00000000000000000000000000000000") 51 | plaintext, _ := hex.DecodeString("") 52 | nonce, _ := hex.DecodeString("00000000000000000000") 53 | ciphertext, _ := hex.DecodeString("") 54 | tag, _ := hex.DecodeString("58e2fccefa7e3061367f1d57a4e7455a") 55 | 56 | _, err := AESGCMEncrypt(nonce, key, plaintext) 57 | assert.Error(t, err) 58 | 59 | _, err = AESGCMDecrypt(nonce, key, append(ciphertext, tag...)) 60 | assert.Error(t, err) 61 | }) 62 | t.Run("bad tag", func(t *testing.T) { 63 | key, _ := hex.DecodeString("00000000000000000000000000000000") 64 | nonce, _ := hex.DecodeString("00000000000000000000") 65 | ciphertext, _ := hex.DecodeString("") 66 | tag, _ := hex.DecodeString("fffffccefa7e3061367f1d57a4e745ff") 67 | 68 | _, err := AESGCMDecrypt(nonce, key, append(ciphertext, tag...)) 69 | assert.Error(t, err) 70 | }) 71 | } 72 | 73 | type failingReader struct { 74 | fails int 75 | reader io.Reader 76 | } 77 | 78 | func (f *failingReader) Read(p []byte) (n int, err error) { 79 | if f.fails > 0 { 80 | f.fails -= 1 81 | return 0, errors.New("no data for you yet") 82 | } else { 83 | return f.reader.Read(p) 84 | } 85 | } 86 | 87 | func TestRandRead(t *testing.T) { 88 | failer := &failingReader{ 89 | fails: 3, 90 | reader: rand.New(rand.NewSource(0)), 91 | } 92 | readBuf := make([]byte, 10) 93 | RandRead(failer, readBuf) 94 | assert.NotEqual(t, [10]byte{}, readBuf) 95 | } 96 | -------------------------------------------------------------------------------- /cmd/ck-server/ck-server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseBindAddr(t *testing.T) { 11 | t.Run("port only", func(t *testing.T) { 12 | addrs, err := resolveBindAddr([]string{":443"}) 13 | assert.NoError(t, err) 14 | assert.Equal(t, ":443", addrs[0].String()) 15 | }) 16 | 17 | t.Run("specific address", func(t *testing.T) { 18 | addrs, err := resolveBindAddr([]string{"192.168.1.123:443"}) 19 | assert.NoError(t, err) 20 | assert.Equal(t, "192.168.1.123:443", addrs[0].String()) 21 | }) 22 | 23 | t.Run("ipv6", func(t *testing.T) { 24 | addrs, err := resolveBindAddr([]string{"[::]:443"}) 25 | assert.NoError(t, err) 26 | assert.Equal(t, "[::]:443", addrs[0].String()) 27 | }) 28 | 29 | t.Run("mixed", func(t *testing.T) { 30 | addrs, err := resolveBindAddr([]string{":80", "[::]:443"}) 31 | assert.NoError(t, err) 32 | assert.Equal(t, ":80", addrs[0].String()) 33 | assert.Equal(t, "[::]:443", addrs[1].String()) 34 | }) 35 | } 36 | 37 | func assertSetEqual(t *testing.T, list1, list2 interface{}, msgAndArgs ...interface{}) (ok bool) { 38 | return assert.Subset(t, list1, list2, msgAndArgs) && assert.Subset(t, list2, list1, msgAndArgs) 39 | } 40 | 41 | func TestParseSSBindAddr(t *testing.T) { 42 | testTable := []struct { 43 | name string 44 | ssRemoteHost string 45 | ssRemotePort string 46 | ckBindAddr []net.Addr 47 | expectedAddr []net.Addr 48 | }{ 49 | { 50 | "ss only ipv4", 51 | "127.0.0.1", 52 | "443", 53 | []net.Addr{}, 54 | []net.Addr{ 55 | &net.TCPAddr{ 56 | IP: net.ParseIP("127.0.0.1"), 57 | Port: 443, 58 | }, 59 | }, 60 | }, 61 | { 62 | "ss only ipv6", 63 | "::", 64 | "443", 65 | []net.Addr{}, 66 | []net.Addr{ 67 | &net.TCPAddr{ 68 | IP: net.ParseIP("::"), 69 | Port: 443, 70 | }, 71 | }, 72 | }, 73 | //{ 74 | // "ss only ipv4 and v6", 75 | // "::|127.0.0.1", 76 | // "443", 77 | // []net.Addr{}, 78 | // []net.Addr{ 79 | // &net.TCPAddr{ 80 | // IP: net.ParseIP("::"), 81 | // Port: 443, 82 | // }, 83 | // &net.TCPAddr{ 84 | // IP: net.ParseIP("127.0.0.1"), 85 | // Port: 443, 86 | // }, 87 | // }, 88 | //}, 89 | { 90 | "ss and existing agrees", 91 | "::", 92 | "443", 93 | []net.Addr{ 94 | &net.TCPAddr{ 95 | IP: net.ParseIP("::"), 96 | Port: 443, 97 | }, 98 | }, 99 | []net.Addr{ 100 | &net.TCPAddr{ 101 | IP: net.ParseIP("::"), 102 | Port: 443, 103 | }, 104 | }, 105 | }, 106 | { 107 | "ss adds onto existing", 108 | "127.0.0.1", 109 | "80", 110 | []net.Addr{ 111 | &net.TCPAddr{ 112 | IP: net.ParseIP("::"), 113 | Port: 443, 114 | }, 115 | }, 116 | []net.Addr{ 117 | &net.TCPAddr{ 118 | IP: net.ParseIP("::"), 119 | Port: 443, 120 | }, 121 | &net.TCPAddr{ 122 | IP: net.ParseIP("127.0.0.1"), 123 | Port: 80, 124 | }, 125 | }, 126 | }, 127 | } 128 | 129 | for _, test := range testTable { 130 | test := test 131 | t.Run(test.name, func(t *testing.T) { 132 | assert.NoError(t, parseSSBindAddr(test.ssRemoteHost, test.ssRemotePort, &test.ckBindAddr)) 133 | assertSetEqual(t, test.ckBindAddr, test.expectedAddr) 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /internal/multiplex/streamBuffer.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | // The data is multiplexed through several TCP connections, therefore the 4 | // order of arrival is not guaranteed. A stream's first packet may be sent through 5 | // connection0 and its second packet may be sent through connection1. Although both 6 | // packets are transmitted reliably (as TCP is reliable), packet1 may arrive to the 7 | // remote side before packet0. Cloak have to therefore sequence the packets so that they 8 | // arrive in order as they were sent by the proxy software 9 | // 10 | // Cloak packets will have a 64-bit sequence number on them, so we know in which order 11 | // they should be sent to the proxy software. The code in this file provides buffering and sorting. 12 | 13 | import ( 14 | "container/heap" 15 | "fmt" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | type sorterHeap []*Frame 21 | 22 | func (sh sorterHeap) Less(i, j int) bool { 23 | return sh[i].Seq < sh[j].Seq 24 | } 25 | func (sh sorterHeap) Len() int { 26 | return len(sh) 27 | } 28 | func (sh sorterHeap) Swap(i, j int) { 29 | sh[i], sh[j] = sh[j], sh[i] 30 | } 31 | 32 | func (sh *sorterHeap) Push(x interface{}) { 33 | *sh = append(*sh, x.(*Frame)) 34 | } 35 | 36 | func (sh *sorterHeap) Pop() interface{} { 37 | old := *sh 38 | n := len(old) 39 | x := old[n-1] 40 | *sh = old[0 : n-1] 41 | return x 42 | } 43 | 44 | type streamBuffer struct { 45 | recvM sync.Mutex 46 | 47 | nextRecvSeq uint64 48 | sh sorterHeap 49 | 50 | buf *streamBufferedPipe 51 | } 52 | 53 | // streamBuffer is a wrapper around streamBufferedPipe. 54 | // Its main function is to sort frames in order, and wait for frames to arrive 55 | // if they have arrived out-of-order. Then it writes the payload of frames into 56 | // a streamBufferedPipe. 57 | func NewStreamBuffer() *streamBuffer { 58 | sb := &streamBuffer{ 59 | sh: []*Frame{}, 60 | buf: NewStreamBufferedPipe(), 61 | } 62 | return sb 63 | } 64 | 65 | func (sb *streamBuffer) Write(f *Frame) (toBeClosed bool, err error) { 66 | sb.recvM.Lock() 67 | defer sb.recvM.Unlock() 68 | // when there'fs no ooo packages in heap and we receive the next package in order 69 | if len(sb.sh) == 0 && f.Seq == sb.nextRecvSeq { 70 | if f.Closing != closingNothing { 71 | return true, nil 72 | } else { 73 | sb.buf.Write(f.Payload) 74 | sb.nextRecvSeq += 1 75 | } 76 | return false, nil 77 | } 78 | 79 | if f.Seq < sb.nextRecvSeq { 80 | return false, fmt.Errorf("seq %v is smaller than nextRecvSeq %v", f.Seq, sb.nextRecvSeq) 81 | } 82 | 83 | saved := *f 84 | saved.Payload = make([]byte, len(f.Payload)) 85 | copy(saved.Payload, f.Payload) 86 | heap.Push(&sb.sh, &saved) 87 | // Keep popping from the heap until empty or to the point that the wanted seq was not received 88 | for len(sb.sh) > 0 && sb.sh[0].Seq == sb.nextRecvSeq { 89 | f = heap.Pop(&sb.sh).(*Frame) 90 | if f.Closing != closingNothing { 91 | return true, nil 92 | } else { 93 | sb.buf.Write(f.Payload) 94 | sb.nextRecvSeq += 1 95 | } 96 | } 97 | return false, nil 98 | } 99 | 100 | func (sb *streamBuffer) Read(buf []byte) (int, error) { 101 | return sb.buf.Read(buf) 102 | } 103 | 104 | func (sb *streamBuffer) Close() error { 105 | sb.recvM.Lock() 106 | defer sb.recvM.Unlock() 107 | 108 | return sb.buf.Close() 109 | } 110 | 111 | func (sb *streamBuffer) SetReadDeadline(t time.Time) { sb.buf.SetReadDeadline(t) } 112 | -------------------------------------------------------------------------------- /internal/ecdh/curve25519_test.go: -------------------------------------------------------------------------------- 1 | // This code is forked from https://github.com/wsddn/go-ecdh/blob/master/curve25519.go 2 | /* 3 | Copyright (c) 2014, tang0th 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of tang0th nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package ecdh 30 | 31 | import ( 32 | "bytes" 33 | "crypto" 34 | "crypto/rand" 35 | "io" 36 | "testing" 37 | ) 38 | 39 | func TestCurve25519(t *testing.T) { 40 | testECDH(t) 41 | } 42 | 43 | func TestErrors(t *testing.T) { 44 | reader, writer := io.Pipe() 45 | _ = writer.Close() 46 | _, _, err := GenerateKey(reader) 47 | if err == nil { 48 | t.Error("GenerateKey should return error") 49 | } 50 | 51 | _, ok := Unmarshal([]byte{1}) 52 | if ok { 53 | t.Error("Unmarshal should return false") 54 | } 55 | } 56 | 57 | func BenchmarkCurve25519(b *testing.B) { 58 | for i := 0; i < b.N; i++ { 59 | testECDH(b) 60 | } 61 | } 62 | 63 | func testECDH(t testing.TB) { 64 | var privKey1, privKey2 crypto.PrivateKey 65 | var pubKey1, pubKey2 crypto.PublicKey 66 | var pubKey1Buf, pubKey2Buf []byte 67 | var err error 68 | var ok bool 69 | var secret1, secret2 []byte 70 | 71 | privKey1, pubKey1, err = GenerateKey(rand.Reader) 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | privKey2, pubKey2, err = GenerateKey(rand.Reader) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | 80 | pubKey1Buf = Marshal(pubKey1) 81 | pubKey2Buf = Marshal(pubKey2) 82 | 83 | pubKey1, ok = Unmarshal(pubKey1Buf) 84 | if !ok { 85 | t.Fatalf("Unmarshal does not work") 86 | } 87 | 88 | pubKey2, ok = Unmarshal(pubKey2Buf) 89 | if !ok { 90 | t.Fatalf("Unmarshal does not work") 91 | } 92 | 93 | secret1, err = GenerateSharedSecret(privKey1, pubKey2) 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | secret2, err = GenerateSharedSecret(privKey2, pubKey1) 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | 102 | if !bytes.Equal(secret1, secret2) { 103 | t.Fatalf("The two shared keys: %d, %d do not match", secret1, secret2) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/server/state_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestParseRedirAddr(t *testing.T) { 9 | t.Run("ipv4 without port", func(t *testing.T) { 10 | ipv4noPort := "1.2.3.4" 11 | host, port, err := parseRedirAddr(ipv4noPort) 12 | if err != nil { 13 | t.Errorf("parsing %v error: %v", ipv4noPort, err) 14 | return 15 | } 16 | if host.String() != "1.2.3.4" { 17 | t.Errorf("expected %v got %v", "1.2.3.4", host.String()) 18 | } 19 | if port != "" { 20 | t.Errorf("port not empty when there is no port") 21 | } 22 | }) 23 | 24 | t.Run("ipv4 with port", func(t *testing.T) { 25 | ipv4wPort := "1.2.3.4:1234" 26 | host, port, err := parseRedirAddr(ipv4wPort) 27 | if err != nil { 28 | t.Errorf("parsing %v error: %v", ipv4wPort, err) 29 | return 30 | } 31 | if host.String() != "1.2.3.4" { 32 | t.Errorf("expected %v got %v", "1.2.3.4", host.String()) 33 | } 34 | if port != "1234" { 35 | t.Errorf("wrong port: expected %v, got %v", "1234", port) 36 | } 37 | }) 38 | 39 | t.Run("domain without port", func(t *testing.T) { 40 | domainNoPort := "example.com" 41 | host, port, err := parseRedirAddr(domainNoPort) 42 | if err != nil { 43 | t.Errorf("parsing %v error: %v", domainNoPort, err) 44 | return 45 | } 46 | 47 | expIPs, err := net.LookupIP("example.com") 48 | if err != nil { 49 | t.Errorf("tester error: cannot resolve example.com: %v", err) 50 | return 51 | } 52 | 53 | contain := false 54 | for _, expIP := range expIPs { 55 | if expIP.String() == host.String() { 56 | contain = true 57 | } 58 | } 59 | 60 | if !contain { 61 | t.Errorf("expected one of %v got %v", expIPs, host.String()) 62 | } 63 | if port != "" { 64 | t.Errorf("port not empty when there is no port") 65 | } 66 | }) 67 | 68 | t.Run("domain with port", func(t *testing.T) { 69 | domainWPort := "example.com:80" 70 | host, port, err := parseRedirAddr(domainWPort) 71 | if err != nil { 72 | t.Errorf("parsing %v error: %v", domainWPort, err) 73 | return 74 | } 75 | 76 | expIPs, err := net.LookupIP("example.com") 77 | if err != nil { 78 | t.Errorf("tester error: cannot resolve example.com: %v", err) 79 | return 80 | } 81 | 82 | contain := false 83 | for _, expIP := range expIPs { 84 | if expIP.String() == host.String() { 85 | contain = true 86 | } 87 | } 88 | 89 | if !contain { 90 | t.Errorf("expected one of %v got %v", expIPs, host.String()) 91 | } 92 | if port != "80" { 93 | t.Errorf("wrong port: expected %v, got %v", "80", port) 94 | } 95 | }) 96 | 97 | t.Run("ipv6 without port", func(t *testing.T) { 98 | ipv6noPort := "a:b:c:d::" 99 | host, port, err := parseRedirAddr(ipv6noPort) 100 | if err != nil { 101 | t.Errorf("parsing %v error: %v", ipv6noPort, err) 102 | return 103 | } 104 | if host.String() != "a:b:c:d::" { 105 | t.Errorf("expected %v got %v", "a:b:c:d::", host.String()) 106 | } 107 | if port != "" { 108 | t.Errorf("port not empty when there is no port") 109 | } 110 | }) 111 | 112 | t.Run("ipv6 with port", func(t *testing.T) { 113 | ipv6wPort := "[a:b:c:d::]:80" 114 | host, port, err := parseRedirAddr(ipv6wPort) 115 | if err != nil { 116 | t.Errorf("parsing %v error: %v", ipv6wPort, err) 117 | return 118 | } 119 | if host.String() != "a:b:c:d::" { 120 | t.Errorf("expected %v got %v", "a:b:c:d::", host.String()) 121 | } 122 | if port != "80" { 123 | t.Errorf("wrong port: expected %v, got %v", "80", port) 124 | } 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /internal/server/activeuser_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "io/ioutil" 7 | "os" 8 | "testing" 9 | 10 | "github.com/cbeuw/Cloak/internal/common" 11 | mux "github.com/cbeuw/Cloak/internal/multiplex" 12 | "github.com/cbeuw/Cloak/internal/server/usermanager" 13 | ) 14 | 15 | func getSeshConfig(unordered bool) mux.SessionConfig { 16 | var sessionKey [32]byte 17 | rand.Read(sessionKey[:]) 18 | obfuscator, _ := mux.MakeObfuscator(0x00, sessionKey) 19 | 20 | seshConfig := mux.SessionConfig{ 21 | Obfuscator: obfuscator, 22 | Valve: nil, 23 | Unordered: unordered, 24 | } 25 | return seshConfig 26 | } 27 | 28 | func TestActiveUser_Bypass(t *testing.T) { 29 | var tmpDB, _ = ioutil.TempFile("", "ck_user_info") 30 | defer os.Remove(tmpDB.Name()) 31 | 32 | manager, err := usermanager.MakeLocalManager(tmpDB.Name(), common.RealWorldState) 33 | if err != nil { 34 | t.Fatal("failed to make local manager", err) 35 | } 36 | panel := MakeUserPanel(manager) 37 | UID, _ := base64.StdEncoding.DecodeString("u97xvcc5YoQA8obCyt9q/w==") 38 | user, _ := panel.GetBypassUser(UID) 39 | var sesh0 *mux.Session 40 | var existing bool 41 | var sesh1 *mux.Session 42 | 43 | // get first session 44 | sesh0, existing, err = user.GetSession(0, getSeshConfig(false)) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if existing { 49 | t.Fatal("get first session: first session returned as existing") 50 | } 51 | if sesh0 == nil { 52 | t.Fatal("get first session: no session returned") 53 | } 54 | 55 | // get first session again 56 | seshx, existing, err := user.GetSession(0, mux.SessionConfig{}) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | if !existing { 61 | t.Fatal("get first session again: first session get again returned as not existing") 62 | } 63 | if seshx == nil { 64 | t.Fatal("get first session again: no session returned") 65 | } 66 | if seshx != sesh0 { 67 | t.Fatal("returned a different instance") 68 | } 69 | 70 | // get second session 71 | sesh1, existing, err = user.GetSession(1, getSeshConfig(false)) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | if existing { 76 | t.Fatal("get second session: second session returned as existing") 77 | } 78 | if sesh1 == nil { 79 | t.Fatal("get second session: no session returned") 80 | } 81 | 82 | if user.NumSession() != 2 { 83 | t.Fatal("number of session is not 2") 84 | } 85 | 86 | user.CloseSession(0, "") 87 | if user.NumSession() != 1 { 88 | t.Fatal("number of session is not 1 after deleting one") 89 | } 90 | if !sesh0.IsClosed() { 91 | t.Fatal("session not closed after deletion") 92 | } 93 | 94 | user.closeAllSessions("") 95 | if !sesh1.IsClosed() { 96 | t.Fatal("session not closed after user termination") 97 | } 98 | 99 | // get session again after termination 100 | seshy, existing, err := user.GetSession(0, getSeshConfig(false)) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | if existing { 105 | t.Fatal("get session again after termination: session returned as existing") 106 | } 107 | if seshy == nil { 108 | t.Fatal("get session again after termination: no session returned") 109 | } 110 | if seshy == sesh0 || seshy == sesh1 { 111 | t.Fatal("get session after termination returned the same instance") 112 | } 113 | 114 | user.CloseSession(0, "") 115 | if panel.isActive(user.arrUID[:]) { 116 | t.Fatal("user still active after last session deleted") 117 | } 118 | 119 | err = manager.Close() 120 | if err != nil { 121 | t.Fatal("failed to close localmanager", err) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /cmd/ck-client/protector_android.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | // +build android 3 | 4 | package main 5 | 6 | // Stolen from https://github.com/shadowsocks/overture/blob/shadowsocks/core/utils/utils_android.go 7 | 8 | /* 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #define ANCIL_FD_BUFFER(n) \ 19 | struct { \ 20 | struct cmsghdr h; \ 21 | int fd[n]; \ 22 | } 23 | 24 | int ancil_send_fds_with_buffer(int sock, const int *fds, unsigned n_fds, 25 | void *buffer) { 26 | struct msghdr msghdr; 27 | char nothing = '!'; 28 | struct iovec nothing_ptr; 29 | struct cmsghdr *cmsg; 30 | int i; 31 | 32 | nothing_ptr.iov_base = ¬hing; 33 | nothing_ptr.iov_len = 1; 34 | msghdr.msg_name = NULL; 35 | msghdr.msg_namelen = 0; 36 | msghdr.msg_iov = ¬hing_ptr; 37 | msghdr.msg_iovlen = 1; 38 | msghdr.msg_flags = 0; 39 | msghdr.msg_control = buffer; 40 | msghdr.msg_controllen = sizeof(struct cmsghdr) + sizeof(int) * n_fds; 41 | cmsg = CMSG_FIRSTHDR(&msghdr); 42 | cmsg->cmsg_len = msghdr.msg_controllen; 43 | cmsg->cmsg_level = SOL_SOCKET; 44 | cmsg->cmsg_type = SCM_RIGHTS; 45 | for (i = 0; i < n_fds; i++) 46 | ((int *)CMSG_DATA(cmsg))[i] = fds[i]; 47 | return (sendmsg(sock, &msghdr, 0) >= 0 ? 0 : -1); 48 | } 49 | 50 | int ancil_send_fd(int sock, int fd) { 51 | ANCIL_FD_BUFFER(1) buffer; 52 | 53 | return (ancil_send_fds_with_buffer(sock, &fd, 1, &buffer)); 54 | } 55 | 56 | void set_timeout(int sock) { 57 | struct timeval tv; 58 | tv.tv_sec = 3; 59 | tv.tv_usec = 0; 60 | setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, 61 | sizeof(struct timeval)); 62 | setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, 63 | sizeof(struct timeval)); 64 | } 65 | */ 66 | import "C" 67 | 68 | import ( 69 | "syscall" 70 | 71 | log "github.com/sirupsen/logrus" 72 | ) 73 | 74 | // In Android, once an app starts the VpnService, all outgoing traffic are routed by the system 75 | // to the VPN app. In our case, the VPN app is ss-local. Our outgoing traffic to ck-server 76 | // will be routed back to ss-local which creates an infinite loop. 77 | // 78 | // The Android system provides an API VpnService.protect(int socketFD) 79 | // This tells the system to bypass the socket around the VPN. 80 | func protector(network string, address string, c syscall.RawConn) error { 81 | log.Println("Using Android VPN mode.") 82 | fn := func(s uintptr) { 83 | fd := int(s) 84 | path := "protect_path" 85 | 86 | socket, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) 87 | if err != nil { 88 | log.Println(err) 89 | return 90 | } 91 | 92 | defer syscall.Close(socket) 93 | 94 | C.set_timeout(C.int(socket)) 95 | 96 | err = syscall.Connect(socket, &syscall.SockaddrUnix{Name: path}) 97 | if err != nil { 98 | log.Println(err) 99 | return 100 | } 101 | 102 | C.ancil_send_fd(C.int(socket), C.int(fd)) 103 | 104 | dummy := []byte{1} 105 | n, err := syscall.Read(socket, dummy) 106 | if err != nil { 107 | log.Println(err) 108 | return 109 | } 110 | if n != 1 { 111 | log.Println("Failed to protect fd: ", fd) 112 | return 113 | } 114 | } 115 | 116 | if err := c.Control(fn); err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: [ push ] 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ ubuntu-latest, macos-latest, windows-latest ] 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-go@v5 12 | with: 13 | go-version: '^1.24' # The Go version to download (if necessary) and use. 14 | - run: go test -race -coverprofile coverage.txt -coverpkg ./... -covermode atomic ./... 15 | - uses: codecov/codecov-action@v4 16 | with: 17 | files: coverage.txt 18 | token: ${{ secrets.CODECOV_TOKEN }} 19 | 20 | compat-test: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | encryption-method: [ plain, chacha20-poly1305 ] 25 | num-conn: [ 0, 1, 4 ] 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-go@v5 29 | with: 30 | go-version: '^1.24' 31 | - name: Build Cloak 32 | run: make 33 | - name: Create configs 34 | run: | 35 | mkdir config 36 | cat << EOF > config/ckclient.json 37 | { 38 | "Transport": "direct", 39 | "ProxyMethod": "iperf", 40 | "EncryptionMethod": "${{ matrix.encryption-method }}", 41 | "UID": "Q4GAXHVgnDLXsdTpw6bmoQ==", 42 | "PublicKey": "4dae/bF43FKGq+QbCc5P/E/MPM5qQeGIArjmJEHiZxc=", 43 | "ServerName": "cloudflare.com", 44 | "BrowserSig": "firefox", 45 | "NumConn": ${{ matrix.num-conn }} 46 | } 47 | EOF 48 | cat << EOF > config/ckserver.json 49 | { 50 | "ProxyBook": { 51 | "iperf": [ 52 | "tcp", 53 | "127.0.0.1:5201" 54 | ] 55 | }, 56 | "BindAddr": [ 57 | ":8443" 58 | ], 59 | "BypassUID": [ 60 | "Q4GAXHVgnDLXsdTpw6bmoQ==" 61 | ], 62 | "RedirAddr": "cloudflare.com", 63 | "PrivateKey": "AAaskZJRPIAbiuaRLHsvZPvE6gzOeSjg+ZRg1ENau0Y=" 64 | } 65 | EOF 66 | - name: Start iperf3 server 67 | run: docker run -d --name iperf-server --network host ajoergensen/iperf3:latest --server 68 | - name: Test new client against old server 69 | run: | 70 | docker run -d --name old-cloak-server --network host -v $PWD/config:/go/Cloak/config cbeuw/cloak:latest build/ck-server -c config/ckserver.json --verbosity debug 71 | build/ck-client -c config/ckclient.json -s 127.0.0.1 -p 8443 --verbosity debug | tee new-cloak-client.log & 72 | docker run --network host ajoergensen/iperf3:latest --client 127.0.0.1 -p 1984 73 | docker stop old-cloak-server 74 | - name: Test old client against new server 75 | run: | 76 | build/ck-server -c config/ckserver.json --verbosity debug | tee new-cloak-server.log & 77 | docker run -d --name old-cloak-client --network host -v $PWD/config:/go/Cloak/config cbeuw/cloak:latest build/ck-client -c config/ckclient.json -s 127.0.0.1 -p 8443 --verbosity debug 78 | docker run --network host ajoergensen/iperf3:latest --client 127.0.0.1 -p 1984 79 | docker stop old-cloak-client 80 | - name: Dump docker logs 81 | if: always() 82 | run: | 83 | docker container logs iperf-server > iperf-server.log 84 | docker container logs old-cloak-server > old-cloak-server.log 85 | docker container logs old-cloak-client > old-cloak-client.log 86 | - name: Upload logs 87 | if: always() 88 | uses: actions/upload-artifact@v4 89 | with: 90 | name: ${{ matrix.encryption-method }}-${{ matrix.num-conn }}-conn-logs 91 | path: ./*.log 92 | -------------------------------------------------------------------------------- /internal/server/usermanager/api_router.go: -------------------------------------------------------------------------------- 1 | package usermanager 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "net/http" 8 | 9 | gmux "github.com/gorilla/mux" 10 | ) 11 | 12 | type APIRouter struct { 13 | *gmux.Router 14 | manager UserManager 15 | } 16 | 17 | func APIRouterOf(manager UserManager) *APIRouter { 18 | ret := &APIRouter{ 19 | manager: manager, 20 | } 21 | ret.registerMux() 22 | return ret 23 | } 24 | 25 | func corsMiddleware(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | w.Header().Set("Access-Control-Allow-Origin", "*") 28 | next.ServeHTTP(w, r) 29 | }) 30 | } 31 | 32 | func (ar *APIRouter) registerMux() { 33 | ar.Router = gmux.NewRouter() 34 | ar.HandleFunc("/admin/users", ar.listAllUsersHlr).Methods("GET") 35 | ar.HandleFunc("/admin/users/{UID}", ar.getUserInfoHlr).Methods("GET") 36 | ar.HandleFunc("/admin/users/{UID}", ar.writeUserInfoHlr).Methods("POST") 37 | ar.HandleFunc("/admin/users/{UID}", ar.deleteUserHlr).Methods("DELETE") 38 | ar.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | w.Header().Set("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS") 40 | }) 41 | ar.Use(corsMiddleware) 42 | } 43 | 44 | func (ar *APIRouter) listAllUsersHlr(w http.ResponseWriter, r *http.Request) { 45 | infos, err := ar.manager.ListAllUsers() 46 | if err != nil { 47 | http.Error(w, err.Error(), http.StatusInternalServerError) 48 | return 49 | } 50 | resp, err := json.Marshal(infos) 51 | if err != nil { 52 | http.Error(w, err.Error(), http.StatusInternalServerError) 53 | return 54 | } 55 | _, _ = w.Write(resp) 56 | } 57 | 58 | func (ar *APIRouter) getUserInfoHlr(w http.ResponseWriter, r *http.Request) { 59 | b64UID := gmux.Vars(r)["UID"] 60 | if b64UID == "" { 61 | http.Error(w, "UID cannot be empty", http.StatusBadRequest) 62 | } 63 | 64 | UID, err := base64.URLEncoding.DecodeString(b64UID) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusBadRequest) 67 | return 68 | } 69 | 70 | uinfo, err := ar.manager.GetUserInfo(UID) 71 | if err == ErrUserNotFound { 72 | http.Error(w, ErrUserNotFound.Error(), http.StatusNotFound) 73 | return 74 | } 75 | resp, err := json.Marshal(uinfo) 76 | if err != nil { 77 | http.Error(w, err.Error(), http.StatusInternalServerError) 78 | return 79 | } 80 | _, _ = w.Write(resp) 81 | } 82 | 83 | func (ar *APIRouter) writeUserInfoHlr(w http.ResponseWriter, r *http.Request) { 84 | b64UID := gmux.Vars(r)["UID"] 85 | if b64UID == "" { 86 | http.Error(w, "UID cannot be empty", http.StatusBadRequest) 87 | return 88 | } 89 | UID, err := base64.URLEncoding.DecodeString(b64UID) 90 | if err != nil { 91 | http.Error(w, err.Error(), http.StatusBadRequest) 92 | return 93 | } 94 | 95 | var uinfo UserInfo 96 | err = json.NewDecoder(r.Body).Decode(&uinfo) 97 | if err != nil { 98 | http.Error(w, err.Error(), http.StatusBadRequest) 99 | return 100 | } 101 | if !bytes.Equal(UID, uinfo.UID) { 102 | http.Error(w, "UID mismatch", http.StatusBadRequest) 103 | } 104 | 105 | err = ar.manager.WriteUserInfo(uinfo) 106 | if err != nil { 107 | http.Error(w, err.Error(), http.StatusInternalServerError) 108 | } 109 | w.WriteHeader(http.StatusCreated) 110 | } 111 | 112 | func (ar *APIRouter) deleteUserHlr(w http.ResponseWriter, r *http.Request) { 113 | b64UID := gmux.Vars(r)["UID"] 114 | if b64UID == "" { 115 | http.Error(w, "UID cannot be empty", http.StatusBadRequest) 116 | return 117 | } 118 | UID, err := base64.URLEncoding.DecodeString(b64UID) 119 | if err != nil { 120 | http.Error(w, err.Error(), http.StatusBadRequest) 121 | return 122 | } 123 | 124 | err = ar.manager.DeleteUser(UID) 125 | if err != nil { 126 | http.Error(w, err.Error(), http.StatusInternalServerError) 127 | } 128 | w.WriteHeader(http.StatusOK) 129 | } 130 | -------------------------------------------------------------------------------- /internal/server/usermanager/api.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | description: | 4 | This is the API of Cloak server 5 | version: 0.0.2 6 | title: Cloak Server 7 | contact: 8 | email: cbeuw.andy@gmail.com 9 | license: 10 | name: GPLv3 11 | url: https://www.gnu.org/licenses/gpl-3.0.en.html 12 | # host: petstore.swagger.io 13 | # basePath: /v2 14 | tags: 15 | - name: users 16 | description: Operations related to user controls by admin 17 | # schemes: 18 | # - http 19 | paths: 20 | /admin/users: 21 | get: 22 | tags: 23 | - users 24 | summary: Show all users 25 | description: Returns an array of all UserInfo 26 | operationId: listAllUsers 27 | produces: 28 | - application/json 29 | responses: 30 | 200: 31 | description: successful operation 32 | schema: 33 | type: array 34 | items: 35 | $ref: '#/definitions/UserInfo' 36 | 500: 37 | description: internal error 38 | /admin/users/{UID}: 39 | get: 40 | tags: 41 | - users 42 | summary: Show userinfo by UID 43 | description: Returns a UserInfo object 44 | operationId: getUserInfo 45 | produces: 46 | - application/json 47 | parameters: 48 | - name: UID 49 | in: path 50 | description: UID of the user 51 | required: true 52 | type: string 53 | format: byte 54 | responses: 55 | 200: 56 | description: successful operation 57 | schema: 58 | $ref: '#/definitions/UserInfo' 59 | 400: 60 | description: bad request 61 | 404: 62 | description: User not found 63 | 500: 64 | description: internal error 65 | post: 66 | tags: 67 | - users 68 | summary: Updates the userinfo of the specified user, if the user does not exist, then a new user is created 69 | operationId: writeUserInfo 70 | consumes: 71 | - application/json 72 | produces: 73 | - application/json 74 | parameters: 75 | - name: UID 76 | in: path 77 | description: UID of the user 78 | required: true 79 | type: string 80 | format: byte 81 | - name: UserInfo 82 | in: body 83 | description: New userinfo 84 | required: true 85 | schema: 86 | type: array 87 | items: 88 | $ref: '#/definitions/UserInfo' 89 | responses: 90 | 201: 91 | description: successful operation 92 | 400: 93 | description: bad request 94 | 500: 95 | description: internal error 96 | delete: 97 | tags: 98 | - users 99 | summary: Deletes a user 100 | operationId: deleteUser 101 | produces: 102 | - application/json 103 | parameters: 104 | - name: UID 105 | in: path 106 | description: UID of the user to be deleted 107 | required: true 108 | type: string 109 | format: byte 110 | responses: 111 | 200: 112 | description: successful operation 113 | 400: 114 | description: bad request 115 | 404: 116 | description: User not found 117 | 500: 118 | description: internal error 119 | 120 | definitions: 121 | UserInfo: 122 | type: object 123 | properties: 124 | UID: 125 | type: string 126 | format: byte 127 | SessionsCap: 128 | type: integer 129 | format: int32 130 | UpRate: 131 | type: integer 132 | format: int64 133 | DownRate: 134 | type: integer 135 | format: int64 136 | UpCredit: 137 | type: integer 138 | format: int64 139 | DownCredit: 140 | type: integer 141 | format: int64 142 | ExpiryTime: 143 | type: integer 144 | format: int64 145 | externalDocs: 146 | description: Find out more about Swagger 147 | url: http://swagger.io 148 | # Added by API Auto Mocking Plugin 149 | host: 127.0.0.1:8080 150 | basePath: / 151 | schemes: 152 | - http -------------------------------------------------------------------------------- /internal/client/piper.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/cbeuw/Cloak/internal/common" 10 | 11 | mux "github.com/cbeuw/Cloak/internal/multiplex" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func RouteUDP(bindFunc func() (*net.UDPConn, error), streamTimeout time.Duration, singleplex bool, newSeshFunc func() *mux.Session) { 16 | var sesh *mux.Session 17 | localConn, err := bindFunc() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | streams := make(map[string]*mux.Stream) 23 | var streamsMutex sync.Mutex 24 | 25 | data := make([]byte, 8192) 26 | for { 27 | i, addr, err := localConn.ReadFrom(data) 28 | if err != nil { 29 | log.Errorf("Failed to read first packet from proxy client: %v", err) 30 | continue 31 | } 32 | 33 | if !singleplex && (sesh == nil || sesh.IsClosed()) { 34 | sesh = newSeshFunc() 35 | } 36 | 37 | streamsMutex.Lock() 38 | stream, ok := streams[addr.String()] 39 | if !ok { 40 | if singleplex { 41 | sesh = newSeshFunc() 42 | } 43 | 44 | stream, err = sesh.OpenStream() 45 | if err != nil { 46 | if singleplex { 47 | sesh.Close() 48 | } 49 | log.Errorf("Failed to open stream: %v", err) 50 | streamsMutex.Unlock() 51 | continue 52 | } 53 | streams[addr.String()] = stream 54 | streamsMutex.Unlock() 55 | 56 | _ = stream.SetReadDeadline(time.Now().Add(streamTimeout)) 57 | 58 | proxyAddr := addr 59 | go func(stream *mux.Stream, localConn *net.UDPConn) { 60 | buf := make([]byte, 8192) 61 | for { 62 | n, err := stream.Read(buf) 63 | if err != nil { 64 | log.Tracef("copying stream to proxy client: %v", err) 65 | break 66 | } 67 | _ = stream.SetReadDeadline(time.Now().Add(streamTimeout)) 68 | 69 | _, err = localConn.WriteTo(buf[:n], proxyAddr) 70 | if err != nil { 71 | log.Tracef("copying stream to proxy client: %v", err) 72 | break 73 | } 74 | } 75 | streamsMutex.Lock() 76 | delete(streams, addr.String()) 77 | streamsMutex.Unlock() 78 | stream.Close() 79 | return 80 | }(stream, localConn) 81 | } else { 82 | streamsMutex.Unlock() 83 | } 84 | 85 | _, err = stream.Write(data[:i]) 86 | if err != nil { 87 | log.Tracef("copying proxy client to stream: %v", err) 88 | streamsMutex.Lock() 89 | delete(streams, addr.String()) 90 | streamsMutex.Unlock() 91 | stream.Close() 92 | continue 93 | } 94 | _ = stream.SetReadDeadline(time.Now().Add(streamTimeout)) 95 | } 96 | } 97 | 98 | func RouteTCP(listener net.Listener, streamTimeout time.Duration, singleplex bool, newSeshFunc func() *mux.Session) { 99 | var sesh *mux.Session 100 | for { 101 | localConn, err := listener.Accept() 102 | if err != nil { 103 | log.Fatal(err) 104 | continue 105 | } 106 | if !singleplex && (sesh == nil || sesh.IsClosed()) { 107 | sesh = newSeshFunc() 108 | } 109 | go func(sesh *mux.Session, localConn net.Conn, timeout time.Duration) { 110 | if singleplex { 111 | sesh = newSeshFunc() 112 | } 113 | 114 | data := make([]byte, 10240) 115 | _ = localConn.SetReadDeadline(time.Now().Add(streamTimeout)) 116 | i, err := io.ReadAtLeast(localConn, data, 1) 117 | if err != nil { 118 | log.Errorf("Failed to read first packet from proxy client: %v", err) 119 | localConn.Close() 120 | return 121 | } 122 | var zeroTime time.Time 123 | _ = localConn.SetReadDeadline(zeroTime) 124 | 125 | stream, err := sesh.OpenStream() 126 | if err != nil { 127 | log.Errorf("Failed to open stream: %v", err) 128 | localConn.Close() 129 | if singleplex { 130 | sesh.Close() 131 | } 132 | return 133 | } 134 | 135 | _, err = stream.Write(data[:i]) 136 | if err != nil { 137 | log.Errorf("Failed to write to stream: %v", err) 138 | localConn.Close() 139 | stream.Close() 140 | return 141 | } 142 | 143 | go func() { 144 | if _, err := common.Copy(localConn, stream); err != nil { 145 | log.Tracef("copying stream to proxy client: %v", err) 146 | } 147 | }() 148 | if _, err = common.Copy(stream, localConn); err != nil { 149 | log.Tracef("copying proxy client to stream: %v", err) 150 | } 151 | }(sesh, localConn, streamTimeout) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /internal/multiplex/mux_test.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "math/rand" 7 | "net" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/cbeuw/Cloak/internal/common" 12 | "github.com/cbeuw/connutil" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func serveEcho(l net.Listener) { 17 | for { 18 | conn, err := l.Accept() 19 | if err != nil { 20 | // TODO: pass the error back 21 | return 22 | } 23 | go func(conn net.Conn) { 24 | _, err := io.Copy(conn, conn) 25 | if err != nil { 26 | // TODO: pass the error back 27 | return 28 | } 29 | }(conn) 30 | } 31 | } 32 | 33 | type connPair struct { 34 | clientConn net.Conn 35 | serverConn net.Conn 36 | } 37 | 38 | func makeSessionPair(numConn int) (*Session, *Session, []*connPair) { 39 | sessionKey := [32]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31} 40 | sessionId := 1 41 | obfuscator, _ := MakeObfuscator(EncryptionMethodChaha20Poly1305, sessionKey) 42 | clientConfig := SessionConfig{ 43 | Obfuscator: obfuscator, 44 | Valve: nil, 45 | Unordered: false, 46 | } 47 | serverConfig := clientConfig 48 | 49 | clientSession := MakeSession(uint32(sessionId), clientConfig) 50 | serverSession := MakeSession(uint32(sessionId), serverConfig) 51 | 52 | paris := make([]*connPair, numConn) 53 | for i := 0; i < numConn; i++ { 54 | c, s := connutil.AsyncPipe() 55 | clientConn := common.NewTLSConn(c) 56 | serverConn := common.NewTLSConn(s) 57 | paris[i] = &connPair{ 58 | clientConn: clientConn, 59 | serverConn: serverConn, 60 | } 61 | clientSession.AddConnection(clientConn) 62 | serverSession.AddConnection(serverConn) 63 | } 64 | return clientSession, serverSession, paris 65 | } 66 | 67 | func runEchoTest(t *testing.T, conns []net.Conn, msgLen int) { 68 | var wg sync.WaitGroup 69 | 70 | for _, conn := range conns { 71 | wg.Add(1) 72 | go func(conn net.Conn) { 73 | defer wg.Done() 74 | 75 | testData := make([]byte, msgLen) 76 | rand.Read(testData) 77 | 78 | // we cannot call t.Fatalf in concurrent contexts 79 | n, err := conn.Write(testData) 80 | if n != msgLen { 81 | t.Errorf("written only %v, err %v", n, err) 82 | return 83 | } 84 | 85 | recvBuf := make([]byte, msgLen) 86 | _, err = io.ReadFull(conn, recvBuf) 87 | if err != nil { 88 | t.Errorf("failed to read back: %v", err) 89 | return 90 | } 91 | 92 | if !bytes.Equal(testData, recvBuf) { 93 | t.Errorf("echoed data not correct") 94 | return 95 | } 96 | }(conn) 97 | } 98 | wg.Wait() 99 | } 100 | 101 | func TestMultiplex(t *testing.T) { 102 | const numStreams = 2000 // -race option limits the number of goroutines to 8192 103 | const numConns = 4 104 | const msgLen = 16384 105 | 106 | clientSession, serverSession, _ := makeSessionPair(numConns) 107 | go serveEcho(serverSession) 108 | 109 | streams := make([]net.Conn, numStreams) 110 | for i := 0; i < numStreams; i++ { 111 | stream, err := clientSession.OpenStream() 112 | assert.NoError(t, err) 113 | streams[i] = stream 114 | } 115 | 116 | //test echo 117 | runEchoTest(t, streams, msgLen) 118 | 119 | assert.EqualValues(t, numStreams, clientSession.streamCount(), "client stream count is wrong") 120 | assert.EqualValues(t, numStreams, serverSession.streamCount(), "server stream count is wrong") 121 | 122 | // close one stream 123 | closing, streams := streams[0], streams[1:] 124 | err := closing.Close() 125 | assert.NoError(t, err, "couldn't close a stream") 126 | _, err = closing.Write([]byte{0}) 127 | assert.Equal(t, ErrBrokenStream, err) 128 | _, err = closing.Read(make([]byte, 1)) 129 | assert.Equal(t, ErrBrokenStream, err) 130 | } 131 | 132 | func TestMux_StreamClosing(t *testing.T) { 133 | clientSession, serverSession, _ := makeSessionPair(1) 134 | go serveEcho(serverSession) 135 | 136 | // read after closing stream 137 | testData := make([]byte, 128) 138 | recvBuf := make([]byte, 128) 139 | toBeClosed, _ := clientSession.OpenStream() 140 | _, err := toBeClosed.Write(testData) // should be echoed back 141 | assert.NoError(t, err, "couldn't write to a stream") 142 | 143 | _, err = io.ReadFull(toBeClosed, recvBuf[:1]) 144 | assert.NoError(t, err, "can't read anything before stream closed") 145 | 146 | _ = toBeClosed.Close() 147 | _, err = io.ReadFull(toBeClosed, recvBuf[1:]) 148 | assert.NoError(t, err, "can't read residual data on stream") 149 | assert.Equal(t, testData, recvBuf, "incorrect data read back") 150 | } 151 | -------------------------------------------------------------------------------- /internal/multiplex/switchboard_test.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | "github.com/cbeuw/connutil" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSwitchboard_Send(t *testing.T) { 15 | doTest := func(seshConfig SessionConfig) { 16 | sesh := MakeSession(0, seshConfig) 17 | hole0 := connutil.Discard() 18 | sesh.sb.addConn(hole0) 19 | conn, err := sesh.sb.pickRandConn() 20 | if err != nil { 21 | t.Error("failed to get a random conn", err) 22 | return 23 | } 24 | data := make([]byte, 1000) 25 | rand.Read(data) 26 | _, err = sesh.sb.send(data, &conn) 27 | if err != nil { 28 | t.Error(err) 29 | return 30 | } 31 | 32 | hole1 := connutil.Discard() 33 | sesh.sb.addConn(hole1) 34 | conn, err = sesh.sb.pickRandConn() 35 | if err != nil { 36 | t.Error("failed to get a random conn", err) 37 | return 38 | } 39 | _, err = sesh.sb.send(data, &conn) 40 | if err != nil { 41 | t.Error(err) 42 | return 43 | } 44 | 45 | conn, err = sesh.sb.pickRandConn() 46 | if err != nil { 47 | t.Error("failed to get a random conn", err) 48 | return 49 | } 50 | _, err = sesh.sb.send(data, &conn) 51 | if err != nil { 52 | t.Error(err) 53 | return 54 | } 55 | } 56 | 57 | t.Run("Ordered", func(t *testing.T) { 58 | seshConfig := SessionConfig{ 59 | Unordered: false, 60 | } 61 | doTest(seshConfig) 62 | }) 63 | t.Run("Unordered", func(t *testing.T) { 64 | seshConfig := SessionConfig{ 65 | Unordered: true, 66 | } 67 | doTest(seshConfig) 68 | }) 69 | } 70 | 71 | func BenchmarkSwitchboard_Send(b *testing.B) { 72 | hole := connutil.Discard() 73 | seshConfig := SessionConfig{} 74 | sesh := MakeSession(0, seshConfig) 75 | sesh.sb.addConn(hole) 76 | conn, err := sesh.sb.pickRandConn() 77 | if err != nil { 78 | b.Error("failed to get a random conn", err) 79 | return 80 | } 81 | data := make([]byte, 1000) 82 | rand.Read(data) 83 | b.SetBytes(int64(len(data))) 84 | b.ResetTimer() 85 | for i := 0; i < b.N; i++ { 86 | sesh.sb.send(data, &conn) 87 | } 88 | } 89 | 90 | func TestSwitchboard_TxCredit(t *testing.T) { 91 | seshConfig := SessionConfig{ 92 | Valve: MakeValve(1<<20, 1<<20), 93 | } 94 | sesh := MakeSession(0, seshConfig) 95 | hole := connutil.Discard() 96 | sesh.sb.addConn(hole) 97 | conn, err := sesh.sb.pickRandConn() 98 | if err != nil { 99 | t.Error("failed to get a random conn", err) 100 | return 101 | } 102 | data := make([]byte, 1000) 103 | rand.Read(data) 104 | 105 | t.Run("fixed conn mapping", func(t *testing.T) { 106 | *sesh.sb.valve.(*LimitedValve).tx = 0 107 | sesh.sb.strategy = fixedConnMapping 108 | n, err := sesh.sb.send(data[:10], &conn) 109 | if err != nil { 110 | t.Error(err) 111 | return 112 | } 113 | if n != 10 { 114 | t.Errorf("wanted to send %v, got %v", 10, n) 115 | return 116 | } 117 | if *sesh.sb.valve.(*LimitedValve).tx != 10 { 118 | t.Error("tx credit didn't increase by 10") 119 | } 120 | }) 121 | t.Run("uniform spread", func(t *testing.T) { 122 | *sesh.sb.valve.(*LimitedValve).tx = 0 123 | sesh.sb.strategy = uniformSpread 124 | n, err := sesh.sb.send(data[:10], &conn) 125 | if err != nil { 126 | t.Error(err) 127 | return 128 | } 129 | if n != 10 { 130 | t.Errorf("wanted to send %v, got %v", 10, n) 131 | return 132 | } 133 | if *sesh.sb.valve.(*LimitedValve).tx != 10 { 134 | t.Error("tx credit didn't increase by 10") 135 | } 136 | }) 137 | } 138 | 139 | func TestSwitchboard_CloseOnOneDisconn(t *testing.T) { 140 | var sessionKey [32]byte 141 | rand.Read(sessionKey[:]) 142 | sesh := setupSesh(false, sessionKey, EncryptionMethodPlain) 143 | 144 | conn0client, conn0server := connutil.AsyncPipe() 145 | sesh.AddConnection(conn0client) 146 | 147 | conn1client, _ := connutil.AsyncPipe() 148 | sesh.AddConnection(conn1client) 149 | 150 | conn0server.Close() 151 | 152 | assert.Eventually(t, func() bool { 153 | return sesh.IsClosed() 154 | }, time.Second, 10*time.Millisecond, "session not closed after one conn is disconnected") 155 | 156 | if _, err := conn1client.Write([]byte{0x00}); err == nil { 157 | t.Error("the other conn is still connected") 158 | return 159 | } 160 | } 161 | 162 | func TestSwitchboard_ConnsCount(t *testing.T) { 163 | seshConfig := SessionConfig{ 164 | Valve: MakeValve(1<<20, 1<<20), 165 | } 166 | sesh := MakeSession(0, seshConfig) 167 | 168 | var wg sync.WaitGroup 169 | for i := 0; i < 1000; i++ { 170 | wg.Add(1) 171 | go func() { 172 | sesh.AddConnection(connutil.Discard()) 173 | wg.Done() 174 | }() 175 | } 176 | wg.Wait() 177 | 178 | if atomic.LoadUint32(&sesh.sb.connsCount) != 1000 { 179 | t.Error("connsCount incorrect") 180 | } 181 | 182 | sesh.sb.closeAll() 183 | 184 | assert.Eventuallyf(t, func() bool { 185 | return atomic.LoadUint32(&sesh.sb.connsCount) == 0 186 | }, time.Second, 10*time.Millisecond, "connsCount incorrect: %v", atomic.LoadUint32(&sesh.sb.connsCount)) 187 | } 188 | -------------------------------------------------------------------------------- /internal/multiplex/switchboard.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "errors" 5 | "github.com/cbeuw/Cloak/internal/common" 6 | log "github.com/sirupsen/logrus" 7 | "math/rand/v2" 8 | "net" 9 | "sync" 10 | "sync/atomic" 11 | ) 12 | 13 | type switchboardStrategy int 14 | 15 | const ( 16 | fixedConnMapping switchboardStrategy = iota 17 | uniformSpread 18 | ) 19 | 20 | // switchboard represents the connection pool. It is responsible for managing 21 | // transport-layer connections between client and server. 22 | // It has several purposes: constantly receiving incoming data from all connections 23 | // and pass them to Session.recvDataFromRemote(); accepting data through 24 | // switchboard.send(), in which it selects a connection according to its 25 | // switchboardStrategy and send the data off using that; and counting, as well as 26 | // rate limiting, data received and sent through its Valve. 27 | type switchboard struct { 28 | session *Session 29 | 30 | valve Valve 31 | strategy switchboardStrategy 32 | 33 | conns sync.Map 34 | connsCount uint32 35 | randPool sync.Pool 36 | 37 | broken uint32 38 | } 39 | 40 | func makeSwitchboard(sesh *Session) *switchboard { 41 | sb := &switchboard{ 42 | session: sesh, 43 | strategy: uniformSpread, 44 | valve: sesh.Valve, 45 | randPool: sync.Pool{New: func() interface{} { 46 | var state [32]byte 47 | common.CryptoRandRead(state[:]) 48 | return rand.New(rand.NewChaCha8(state)) 49 | }}, 50 | } 51 | return sb 52 | } 53 | 54 | var errBrokenSwitchboard = errors.New("the switchboard is broken") 55 | 56 | func (sb *switchboard) addConn(conn net.Conn) { 57 | connId := atomic.AddUint32(&sb.connsCount, 1) - 1 58 | sb.conns.Store(connId, conn) 59 | go sb.deplex(conn) 60 | } 61 | 62 | // a pointer to assignedConn is passed here so that the switchboard can reassign it if that conn isn't usable 63 | func (sb *switchboard) send(data []byte, assignedConn *net.Conn) (n int, err error) { 64 | sb.valve.txWait(len(data)) 65 | if atomic.LoadUint32(&sb.broken) == 1 { 66 | return 0, errBrokenSwitchboard 67 | } 68 | 69 | var conn net.Conn 70 | switch sb.strategy { 71 | case uniformSpread: 72 | conn, err = sb.pickRandConn() 73 | if err != nil { 74 | return 0, errBrokenSwitchboard 75 | } 76 | n, err = conn.Write(data) 77 | if err != nil { 78 | sb.session.SetTerminalMsg("failed to send to remote " + err.Error()) 79 | sb.session.passiveClose() 80 | return n, err 81 | } 82 | case fixedConnMapping: 83 | // FIXME: this strategy has a tendency to cause a TLS conn socket buffer to fill up, 84 | // which is a problem when multiple streams are mapped to the same conn, resulting 85 | // in all such streams being blocked. 86 | conn = *assignedConn 87 | if conn == nil { 88 | conn, err = sb.pickRandConn() 89 | if err != nil { 90 | sb.session.SetTerminalMsg("failed to pick a connection " + err.Error()) 91 | sb.session.passiveClose() 92 | return 0, err 93 | } 94 | *assignedConn = conn 95 | } 96 | n, err = conn.Write(data) 97 | if err != nil { 98 | sb.session.SetTerminalMsg("failed to send to remote " + err.Error()) 99 | sb.session.passiveClose() 100 | return n, err 101 | } 102 | default: 103 | return 0, errors.New("unsupported traffic distribution strategy") 104 | } 105 | 106 | sb.valve.AddTx(int64(n)) 107 | return n, nil 108 | } 109 | 110 | // returns a random conn. This function can be called concurrently. 111 | func (sb *switchboard) pickRandConn() (net.Conn, error) { 112 | if atomic.LoadUint32(&sb.broken) == 1 { 113 | return nil, errBrokenSwitchboard 114 | } 115 | 116 | connsCount := atomic.LoadUint32(&sb.connsCount) 117 | if connsCount == 0 { 118 | return nil, errBrokenSwitchboard 119 | } 120 | 121 | randReader := sb.randPool.Get().(*rand.Rand) 122 | connId := randReader.Uint32N(connsCount) 123 | sb.randPool.Put(randReader) 124 | 125 | ret, ok := sb.conns.Load(connId) 126 | if !ok { 127 | log.Errorf("failed to get conn %d", connId) 128 | return nil, errBrokenSwitchboard 129 | } 130 | return ret.(net.Conn), nil 131 | } 132 | 133 | // actively triggered by session.Close() 134 | func (sb *switchboard) closeAll() { 135 | if !atomic.CompareAndSwapUint32(&sb.broken, 0, 1) { 136 | return 137 | } 138 | atomic.StoreUint32(&sb.connsCount, 0) 139 | sb.conns.Range(func(_, conn interface{}) bool { 140 | conn.(net.Conn).Close() 141 | sb.conns.Delete(conn) 142 | return true 143 | }) 144 | } 145 | 146 | // deplex function costantly reads from a TCP connection 147 | func (sb *switchboard) deplex(conn net.Conn) { 148 | defer conn.Close() 149 | buf := make([]byte, sb.session.connReceiveBufferSize) 150 | for { 151 | n, err := conn.Read(buf) 152 | sb.valve.rxWait(n) 153 | sb.valve.AddRx(int64(n)) 154 | if err != nil { 155 | log.Debugf("a connection for session %v has closed: %v", sb.session.id, err) 156 | sb.session.SetTerminalMsg("a connection has dropped unexpectedly") 157 | sb.session.passiveClose() 158 | return 159 | } 160 | 161 | err = sb.session.recvDataFromRemote(buf[:n]) 162 | if err != nil { 163 | log.Error(err) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs= 4 | github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE= 5 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 6 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 12 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 13 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 14 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 15 | github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= 16 | github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= 17 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 18 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 19 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 20 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 21 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 22 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 24 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 25 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 26 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= 30 | github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= 31 | github.com/refraction-networking/utls v1.7.0/go.mod h1:lV0Gwc1/Fi+HYH8hOtgFRdHfKo4FKSn6+FdyOz9hRms= 32 | github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo= 33 | github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ= 34 | github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= 35 | github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= 36 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 37 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 38 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 39 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 40 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 41 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 42 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 44 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 45 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 46 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 47 | go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 48 | go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= 49 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 50 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 51 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 52 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 53 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 55 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 58 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 59 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 60 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 61 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 62 | -------------------------------------------------------------------------------- /internal/client/TLS.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/cbeuw/Cloak/internal/common" 5 | utls "github.com/refraction-networking/utls" 6 | log "github.com/sirupsen/logrus" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | const appDataMaxLength = 16401 12 | 13 | type clientHelloFields struct { 14 | random []byte 15 | sessionId []byte 16 | x25519KeyShare []byte 17 | serverName string 18 | } 19 | 20 | type browser int 21 | 22 | const ( 23 | chrome = iota 24 | firefox 25 | safari 26 | ) 27 | 28 | type DirectTLS struct { 29 | *common.TLSConn 30 | browser browser 31 | } 32 | 33 | var topLevelDomains = []string{"com", "net", "org", "it", "fr", "me", "ru", "cn", "es", "tr", "top", "xyz", "info"} 34 | 35 | func randomServerName() string { 36 | /* 37 | Copyright: Proton AG 38 | https://github.com/ProtonVPN/wireguard-go/commit/bcf344b39b213c1f32147851af0d2a8da9266883 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining a copy of 41 | this software and associated documentation files (the "Software"), to deal in 42 | the Software without restriction, including without limitation the rights to 43 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 44 | of the Software, and to permit persons to whom the Software is furnished to do 45 | so, subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all 48 | copies or substantial portions of the Software. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 51 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 52 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 53 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 54 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 55 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 56 | SOFTWARE. 57 | */ 58 | charNum := int('z') - int('a') + 1 59 | size := 3 + common.RandInt(10) 60 | name := make([]byte, size) 61 | for i := range name { 62 | name[i] = byte(int('a') + common.RandInt(charNum)) 63 | } 64 | return string(name) + "." + common.RandItem(topLevelDomains) 65 | } 66 | 67 | func buildClientHello(browser browser, fields clientHelloFields) ([]byte, error) { 68 | // We don't use utls to handle connections (as it'll attempt a real TLS negotiation) 69 | // We only want it to build the ClientHello locally 70 | fakeConn := net.TCPConn{} 71 | var helloID utls.ClientHelloID 72 | switch browser { 73 | case chrome: 74 | helloID = utls.HelloChrome_Auto 75 | case firefox: 76 | helloID = utls.HelloFirefox_Auto 77 | case safari: 78 | helloID = utls.HelloSafari_Auto 79 | } 80 | 81 | uclient := utls.UClient(&fakeConn, &utls.Config{ServerName: fields.serverName}, helloID) 82 | if err := uclient.BuildHandshakeState(); err != nil { 83 | return []byte{}, err 84 | } 85 | if err := uclient.SetClientRandom(fields.random); err != nil { 86 | return []byte{}, err 87 | } 88 | 89 | uclient.HandshakeState.Hello.SessionId = make([]byte, 32) 90 | copy(uclient.HandshakeState.Hello.SessionId, fields.sessionId) 91 | 92 | // Find the X25519 key share and overwrite it 93 | var extIndex int 94 | var keyShareIndex int 95 | for i, ext := range uclient.Extensions { 96 | ext, ok := ext.(*utls.KeyShareExtension) 97 | if ok { 98 | extIndex = i 99 | for j, keyShare := range ext.KeyShares { 100 | if keyShare.Group == utls.X25519 { 101 | keyShareIndex = j 102 | } 103 | } 104 | } 105 | } 106 | copy(uclient.Extensions[extIndex].(*utls.KeyShareExtension).KeyShares[keyShareIndex].Data, fields.x25519KeyShare) 107 | 108 | if err := uclient.BuildHandshakeState(); err != nil { 109 | return []byte{}, err 110 | } 111 | return uclient.HandshakeState.Hello.Raw, nil 112 | } 113 | 114 | // Handshake handles the TLS handshake for a given conn and returns the sessionKey 115 | // if the server proceed with Cloak authentication 116 | func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) { 117 | payload, sharedSecret := makeAuthenticationPayload(authInfo) 118 | 119 | fields := clientHelloFields{ 120 | random: payload.randPubKey[:], 121 | sessionId: payload.ciphertextWithTag[0:32], 122 | x25519KeyShare: payload.ciphertextWithTag[32:64], 123 | serverName: authInfo.MockDomain, 124 | } 125 | 126 | if strings.EqualFold(fields.serverName, "random") { 127 | fields.serverName = randomServerName() 128 | } 129 | 130 | var ch []byte 131 | ch, err = buildClientHello(tls.browser, fields) 132 | if err != nil { 133 | return 134 | } 135 | chWithRecordLayer := common.AddRecordLayer(ch, common.Handshake, common.VersionTLS11) 136 | _, err = rawConn.Write(chWithRecordLayer) 137 | if err != nil { 138 | return 139 | } 140 | log.Trace("client hello sent successfully") 141 | tls.TLSConn = common.NewTLSConn(rawConn) 142 | 143 | buf := make([]byte, 1024) 144 | log.Trace("waiting for ServerHello") 145 | _, err = tls.Read(buf) 146 | if err != nil { 147 | return 148 | } 149 | 150 | encrypted := append(buf[6:38], buf[84:116]...) 151 | nonce := encrypted[0:12] 152 | ciphertextWithTag := encrypted[12:60] 153 | sessionKeySlice, err := common.AESGCMDecrypt(nonce, sharedSecret[:], ciphertextWithTag) 154 | if err != nil { 155 | return 156 | } 157 | copy(sessionKey[:], sessionKeySlice) 158 | 159 | for i := 0; i < 2; i++ { 160 | // ChangeCipherSpec and EncryptedCert (in the format of application data) 161 | _, err = tls.Read(buf) 162 | if err != nil { 163 | return 164 | } 165 | } 166 | return sessionKey, nil 167 | 168 | } 169 | -------------------------------------------------------------------------------- /internal/server/TLSAux_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | ) 8 | 9 | func TestParseClientHello(t *testing.T) { 10 | t.Run("good Cloak ClientHello", func(t *testing.T) { 11 | chBytes, _ := hex.DecodeString("1603010200010001fc03034986187cfaf4c55866a0d9b68f82505fd694a3f0fbf21ca3dcf260baad91d75e20c10e2d2c66f4f9366296678550ed769aa0c41cae7e5f480f59bd929b747ee48d0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00208d7d5a544a72e67adb1bacde46aa147b086f714c073f8335688dc13b2a032986001700414e06fb9a27480a93159f3d6273afebb4d307c4a734d7107d883b6edacb58f7d289a95ad8aaedef1b5f76fe09267a14e6bee2b6db4506b43cf0a410a4645105f79f002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 12 | ch, err := parseClientHello(chBytes) 13 | if err != nil { 14 | t.Errorf("Expecting no error, got %v", err) 15 | return 16 | } 17 | if !bytes.Equal(ch.clientVersion, []byte{0x03, 0x03}) { 18 | t.Errorf("expecting client version 0x0303, got %v", ch.clientVersion) 19 | return 20 | } 21 | }) 22 | t.Run("Malformed ClientHello", func(t *testing.T) { 23 | chBytes, _ := hex.DecodeString("1603010200010001fc03034986187cfaf4c55866a0d9b68f82505fd694a3f0fb2f21ca3dcf260baad91d75e20c10e2d2c66f4f9366296678550ed769aa0c41cae7e5f480f59bd929b747ee48d0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00208d7d5a544a72e67adb1bacde46aa147b086f714c073f8335688dc13b2a032986001700414e06fb9a27480a93159f3d6273afebb4d307c4a734d7107d883b6edacb58f7d289a95ad8aaedef1b5f76fe09267a14e6bee2b6db4506b43cf0a410a4645105f79f002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 24 | _, err := parseClientHello(chBytes) 25 | if err == nil { 26 | t.Error("expecting Malformed ClientHello, got no error") 27 | return 28 | } 29 | }) 30 | t.Run("not Handshake", func(t *testing.T) { 31 | chBytes, _ := hex.DecodeString("ff03010200010001fc03034986187cfaf4c55866a0d9b68f82505fd694a3f0fbf21ca3dcf260baad91d75e20c10e2d2c66f4f9366296678550ed769aa0c41cae7e5f480f59bd929b747ee48d0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00208d7d5a544a72e67adb1bacde46aa147b086f714c073f8335688dc13b2a032986001700414e06fb9a27480a93159f3d6273afebb4d307c4a734d7107d883b6edacb58f7d289a95ad8aaedef1b5f76fe09267a14e6bee2b6db4506b43cf0a410a4645105f79f002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 32 | _, err := parseClientHello(chBytes) 33 | if err == nil { 34 | t.Error("not a tls handshake, got no error") 35 | return 36 | } 37 | }) 38 | t.Run("wrong TLS record layer version", func(t *testing.T) { 39 | chBytes, _ := hex.DecodeString("16ff010200010001fc03034986187cfaf4c55866a0d9b68f82505fd694a3f0fbf21ca3dcf260baad91d75e20c10e2d2c66f4f9366296678550ed769aa0c41cae7e5f480f59bd929b747ee48d0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00208d7d5a544a72e67adb1bacde46aa147b086f714c073f8335688dc13b2a032986001700414e06fb9a27480a93159f3d6273afebb4d307c4a734d7107d883b6edacb58f7d289a95ad8aaedef1b5f76fe09267a14e6bee2b6db4506b43cf0a410a4645105f79f002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 40 | _, err := parseClientHello(chBytes) 41 | if err == nil { 42 | t.Error("wrong version, got no error") 43 | return 44 | } 45 | }) 46 | t.Run("TLS 1.2", func(t *testing.T) { 47 | chBytes, _ := hex.DecodeString("16030300bd010000b903035d5741ed86719917a932db1dc59a22c7166bf90f5bd693564341d091ffbac5db00002ac02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c013009d009c003d003c0035002f000a0100006600000022002000001d6e61762e736d61727473637265656e2e6d6963726f736f66742e636f6d000500050100000000000a00080006001d00170018000b00020100000d001400120401050102010403050302030202060106030023000000170000ff01000100") 48 | _, err := parseClientHello(chBytes) 49 | if err == nil { 50 | t.Error("wrong version, got no error") 51 | return 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /internal/server/userpanel_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/base64" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/cbeuw/Cloak/internal/common" 11 | "github.com/cbeuw/Cloak/internal/server/usermanager" 12 | ) 13 | 14 | func TestUserPanel_BypassUser(t *testing.T) { 15 | var tmpDB, _ = ioutil.TempFile("", "ck_user_info") 16 | defer os.Remove(tmpDB.Name()) 17 | 18 | manager, err := usermanager.MakeLocalManager(tmpDB.Name(), common.RealWorldState) 19 | if err != nil { 20 | t.Error("failed to make local manager", err) 21 | } 22 | panel := MakeUserPanel(manager) 23 | UID, _ := base64.StdEncoding.DecodeString("u97xvcc5YoQA8obCyt9q/w==") 24 | user, _ := panel.GetBypassUser(UID) 25 | user.valve.AddRx(10) 26 | user.valve.AddTx(10) 27 | t.Run("isActive", func(t *testing.T) { 28 | a := panel.isActive(UID) 29 | if !a { 30 | t.Error("isActive returned ", a) 31 | } 32 | }) 33 | t.Run("updateUsageQueue", func(t *testing.T) { 34 | panel.updateUsageQueue() 35 | if _, inQ := panel.usageUpdateQueue[user.arrUID]; inQ { 36 | t.Error("user in update queue") 37 | } 38 | }) 39 | t.Run("updateUsageQueueForOne", func(t *testing.T) { 40 | panel.updateUsageQueueForOne(user) 41 | if _, inQ := panel.usageUpdateQueue[user.arrUID]; inQ { 42 | t.Error("user in update queue") 43 | } 44 | }) 45 | t.Run("commitUpdate", func(t *testing.T) { 46 | err := panel.commitUpdate() 47 | if err != nil { 48 | t.Error("commit returned", err) 49 | } 50 | }) 51 | t.Run("TerminateActiveUser", func(t *testing.T) { 52 | panel.TerminateActiveUser(user, "") 53 | if panel.isActive(user.arrUID[:]) { 54 | t.Error("user still active after deletion", err) 55 | } 56 | }) 57 | t.Run("Repeated delete", func(t *testing.T) { 58 | panel.TerminateActiveUser(user, "") 59 | }) 60 | err = manager.Close() 61 | if err != nil { 62 | t.Error("failed to close localmanager", err) 63 | } 64 | } 65 | 66 | var mockUID = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} 67 | var mockWorldState = common.WorldOfTime(time.Unix(1, 0)) 68 | var validUserInfo = usermanager.UserInfo{ 69 | UID: mockUID, 70 | SessionsCap: usermanager.JustInt32(10), 71 | UpRate: usermanager.JustInt64(100), 72 | DownRate: usermanager.JustInt64(1000), 73 | UpCredit: usermanager.JustInt64(10000), 74 | DownCredit: usermanager.JustInt64(100000), 75 | ExpiryTime: usermanager.JustInt64(1000000), 76 | } 77 | 78 | func TestUserPanel_GetUser(t *testing.T) { 79 | var tmpDB, _ = ioutil.TempFile("", "ck_user_info") 80 | defer os.Remove(tmpDB.Name()) 81 | mgr, err := usermanager.MakeLocalManager(tmpDB.Name(), mockWorldState) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | panel := MakeUserPanel(mgr) 86 | 87 | t.Run("normal user", func(t *testing.T) { 88 | _ = mgr.WriteUserInfo(validUserInfo) 89 | 90 | activeUser, err := panel.GetUser(validUserInfo.UID) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | again, err := panel.GetUser(validUserInfo.UID) 96 | if err != nil { 97 | t.Errorf("can't get existing user: %v", err) 98 | } 99 | 100 | if activeUser != again { 101 | t.Error("got different references") 102 | } 103 | }) 104 | t.Run("non existent user", func(t *testing.T) { 105 | _, err = panel.GetUser(make([]byte, 16)) 106 | if err != usermanager.ErrUserNotFound { 107 | t.Errorf("expecting error %v, got %v", usermanager.ErrUserNotFound, err) 108 | } 109 | }) 110 | } 111 | 112 | func TestUserPanel_UpdateUsageQueue(t *testing.T) { 113 | var tmpDB, _ = ioutil.TempFile("", "ck_user_info") 114 | defer os.Remove(tmpDB.Name()) 115 | mgr, err := usermanager.MakeLocalManager(tmpDB.Name(), mockWorldState) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | panel := MakeUserPanel(mgr) 120 | 121 | t.Run("normal update", func(t *testing.T) { 122 | _ = mgr.WriteUserInfo(validUserInfo) 123 | 124 | user, err := panel.GetUser(validUserInfo.UID) 125 | if err != nil { 126 | t.Error(err) 127 | } 128 | 129 | user.valve.AddTx(1) 130 | user.valve.AddRx(2) 131 | panel.updateUsageQueue() 132 | err = panel.commitUpdate() 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | 137 | if user.valve.GetRx() != 0 || user.valve.GetTx() != 0 { 138 | t.Error("rx and tx stats are not cleared") 139 | } 140 | 141 | updatedUinfo, _ := mgr.GetUserInfo(validUserInfo.UID) 142 | if *updatedUinfo.DownCredit != *validUserInfo.DownCredit-1 { 143 | t.Error("down credit incorrect update") 144 | } 145 | if *updatedUinfo.UpCredit != *validUserInfo.UpCredit-2 { 146 | t.Error("up credit incorrect update") 147 | } 148 | 149 | // another update 150 | user.valve.AddTx(3) 151 | user.valve.AddRx(4) 152 | panel.updateUsageQueue() 153 | err = panel.commitUpdate() 154 | if err != nil { 155 | t.Error(err) 156 | } 157 | 158 | updatedUinfo, _ = mgr.GetUserInfo(validUserInfo.UID) 159 | if *updatedUinfo.DownCredit != *validUserInfo.DownCredit-(1+3) { 160 | t.Error("down credit incorrect update") 161 | } 162 | if *updatedUinfo.UpCredit != *validUserInfo.UpCredit-(2+4) { 163 | t.Error("up credit incorrect update") 164 | } 165 | }) 166 | t.Run("terminating update", func(t *testing.T) { 167 | _ = mgr.WriteUserInfo(validUserInfo) 168 | 169 | user, err := panel.GetUser(validUserInfo.UID) 170 | if err != nil { 171 | t.Error(err) 172 | } 173 | 174 | user.valve.AddTx(*validUserInfo.DownCredit + 100) 175 | panel.updateUsageQueue() 176 | err = panel.commitUpdate() 177 | if err != nil { 178 | t.Error(err) 179 | } 180 | 181 | if panel.isActive(validUserInfo.UID) { 182 | t.Error("user not terminated") 183 | } 184 | 185 | updatedUinfo, _ := mgr.GetUserInfo(validUserInfo.UID) 186 | if *updatedUinfo.DownCredit != -100 { 187 | t.Error("down credit not updated correctly after the user has been terminated") 188 | } 189 | }) 190 | } 191 | -------------------------------------------------------------------------------- /cmd/ck-server/ck-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | _ "net/http/pprof" 9 | "os" 10 | "runtime" 11 | "strings" 12 | 13 | "github.com/cbeuw/Cloak/internal/common" 14 | "github.com/cbeuw/Cloak/internal/server" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var version string 19 | 20 | func resolveBindAddr(bindAddrs []string) ([]net.Addr, error) { 21 | var addrs []net.Addr 22 | for _, addr := range bindAddrs { 23 | bindAddr, err := net.ResolveTCPAddr("tcp", addr) 24 | if err != nil { 25 | return nil, err 26 | } 27 | addrs = append(addrs, bindAddr) 28 | } 29 | return addrs, nil 30 | } 31 | 32 | // parse what shadowsocks server wants us to bind and harmonise it with what's already in bindAddr from 33 | // our own config's BindAddr. This prevents duplicate bindings etc. 34 | func parseSSBindAddr(ssRemoteHost string, ssRemotePort string, ckBindAddr *[]net.Addr) error { 35 | var ssBind string 36 | // When listening on an IPv6 and IPv4, SS gives REMOTE_HOST as e.g. ::|0.0.0.0 37 | v4nv6 := len(strings.Split(ssRemoteHost, "|")) == 2 38 | if v4nv6 { 39 | ssBind = ":" + ssRemotePort 40 | } else { 41 | ssBind = net.JoinHostPort(ssRemoteHost, ssRemotePort) 42 | } 43 | ssBindAddr, err := net.ResolveTCPAddr("tcp", ssBind) 44 | if err != nil { 45 | return fmt.Errorf("unable to resolve bind address provided by SS: %v", err) 46 | } 47 | 48 | shouldAppend := true 49 | for i, addr := range *ckBindAddr { 50 | if addr.String() == ssBindAddr.String() { 51 | shouldAppend = false 52 | } 53 | if addr.String() == ":"+ssRemotePort { // already listening on all interfaces 54 | shouldAppend = false 55 | } 56 | if addr.String() == "0.0.0.0:"+ssRemotePort || addr.String() == "[::]:"+ssRemotePort { 57 | // if config listens on one ip version but ss wants to listen on both, 58 | // listen on both 59 | if ssBindAddr.String() == ":"+ssRemotePort { 60 | shouldAppend = true 61 | (*ckBindAddr)[i] = ssBindAddr 62 | } 63 | } 64 | } 65 | if shouldAppend { 66 | *ckBindAddr = append(*ckBindAddr, ssBindAddr) 67 | } 68 | return nil 69 | } 70 | 71 | func main() { 72 | var config string 73 | 74 | var pluginMode bool 75 | 76 | log.SetFormatter(&log.TextFormatter{ 77 | FullTimestamp: true, 78 | }) 79 | 80 | if os.Getenv("SS_LOCAL_HOST") != "" && os.Getenv("SS_LOCAL_PORT") != "" { 81 | pluginMode = true 82 | config = os.Getenv("SS_PLUGIN_OPTIONS") 83 | } else { 84 | flag.StringVar(&config, "c", "server.json", "config: path to the configuration file or its content") 85 | askVersion := flag.Bool("v", false, "Print the version number") 86 | printUsage := flag.Bool("h", false, "Print this message") 87 | 88 | genUIDScript := flag.Bool("u", false, "Generate a UID to STDOUT") 89 | genKeyPairScript := flag.Bool("k", false, "Generate a pair of public and private key and output to STDOUT in the format of ,") 90 | 91 | genUIDHuman := flag.Bool("uid", false, "Generate and print out a UID") 92 | genKeyPairHuman := flag.Bool("key", false, "Generate and print out a public-private key pair") 93 | 94 | pprofAddr := flag.String("d", "", "debug use: ip:port to be listened by pprof profiler") 95 | verbosity := flag.String("verbosity", "info", "verbosity level") 96 | 97 | flag.Parse() 98 | 99 | if *askVersion { 100 | fmt.Printf("ck-server %s", version) 101 | return 102 | } 103 | if *printUsage { 104 | flag.Usage() 105 | return 106 | } 107 | if *genUIDScript || *genUIDHuman { 108 | uid := generateUID() 109 | if *genUIDScript { 110 | fmt.Println(uid) 111 | } else { 112 | fmt.Printf("\x1B[35mYour UID is:\u001B[0m %s\n", uid) 113 | } 114 | return 115 | } 116 | if *genKeyPairScript || *genKeyPairHuman { 117 | pub, pv := generateKeyPair() 118 | if *genKeyPairScript { 119 | fmt.Printf("%v,%v\n", pub, pv) 120 | } else { 121 | fmt.Printf("\x1B[36mYour PUBLIC key is:\x1B[0m %65s\n", pub) 122 | fmt.Printf("\x1B[33mYour PRIVATE key is (keep it secret):\x1B[0m %47s\n", pv) 123 | } 124 | return 125 | } 126 | 127 | if *pprofAddr != "" { 128 | runtime.SetBlockProfileRate(5) 129 | go func() { 130 | log.Info(http.ListenAndServe(*pprofAddr, nil)) 131 | }() 132 | log.Infof("pprof listening on %v", *pprofAddr) 133 | 134 | } 135 | 136 | lvl, err := log.ParseLevel(*verbosity) 137 | if err != nil { 138 | log.Fatal(err) 139 | } 140 | log.SetLevel(lvl) 141 | 142 | log.Infof("Starting standalone mode") 143 | } 144 | 145 | raw, err := server.ParseConfig(config) 146 | if err != nil { 147 | log.Fatalf("Configuration file error: %v", err) 148 | } 149 | 150 | bindAddr, err := resolveBindAddr(raw.BindAddr) 151 | if err != nil { 152 | log.Fatalf("unable to parse BindAddr: %v", err) 153 | } 154 | 155 | // in case the user hasn't specified any local address to bind to, we listen on 443 and 80 156 | if !pluginMode && len(bindAddr) == 0 { 157 | https, _ := net.ResolveTCPAddr("tcp", ":443") 158 | http, _ := net.ResolveTCPAddr("tcp", ":80") 159 | bindAddr = []net.Addr{https, http} 160 | } 161 | 162 | // when cloak is started as a shadowsocks plugin, we parse the address ss-server 163 | // is listening on into ProxyBook, and we parse the list of bindAddr 164 | if pluginMode { 165 | ssLocalHost := os.Getenv("SS_LOCAL_HOST") 166 | ssLocalPort := os.Getenv("SS_LOCAL_PORT") 167 | raw.ProxyBook["shadowsocks"] = []string{"tcp", net.JoinHostPort(ssLocalHost, ssLocalPort)} 168 | 169 | ssRemoteHost := os.Getenv("SS_REMOTE_HOST") 170 | ssRemotePort := os.Getenv("SS_REMOTE_PORT") 171 | err = parseSSBindAddr(ssRemoteHost, ssRemotePort, &bindAddr) 172 | if err != nil { 173 | log.Fatalf("failed to parse SS_REMOTE_HOST and SS_REMOTE_PORT: %v", err) 174 | } 175 | } 176 | 177 | sta, err := server.InitState(raw, common.RealWorldState) 178 | if err != nil { 179 | log.Fatalf("unable to initialise server state: %v", err) 180 | } 181 | 182 | listen := func(bindAddr net.Addr) { 183 | listener, err := net.Listen("tcp", bindAddr.String()) 184 | log.Infof("Listening on %v", bindAddr) 185 | if err != nil { 186 | log.Fatal(err) 187 | } 188 | server.Serve(listener, sta) 189 | } 190 | 191 | for i, addr := range bindAddr { 192 | if i != len(bindAddr)-1 { 193 | go listen(addr) 194 | } else { 195 | // we block the main goroutine here so it doesn't quit 196 | listen(addr) 197 | } 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /cmd/ck-client/ck-client.go: -------------------------------------------------------------------------------- 1 | //go:build go1.11 2 | // +build go1.11 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/base64" 8 | "encoding/binary" 9 | "flag" 10 | "fmt" 11 | "net" 12 | "os" 13 | 14 | "github.com/cbeuw/Cloak/internal/common" 15 | 16 | "github.com/cbeuw/Cloak/internal/client" 17 | mux "github.com/cbeuw/Cloak/internal/multiplex" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var version string 22 | 23 | func main() { 24 | // Should be 127.0.0.1 to listen to a proxy client on this machine 25 | var localHost string 26 | // port used by proxy clients to communicate with cloak client 27 | var localPort string 28 | // The ip of the proxy server 29 | var remoteHost string 30 | // The proxy port,should be 443 31 | var remotePort string 32 | var proxyMethod string 33 | var udp bool 34 | var config string 35 | var b64AdminUID string 36 | var vpnMode bool 37 | var tcpFastOpen bool 38 | 39 | log_init() 40 | 41 | ssPluginMode := os.Getenv("SS_LOCAL_HOST") != "" 42 | 43 | verbosity := flag.String("verbosity", "info", "verbosity level") 44 | if ssPluginMode { 45 | config = os.Getenv("SS_PLUGIN_OPTIONS") 46 | flag.BoolVar(&vpnMode, "V", false, "ignored.") 47 | flag.BoolVar(&tcpFastOpen, "fast-open", false, "ignored.") 48 | flag.Parse() // for verbosity only 49 | } else { 50 | flag.StringVar(&localHost, "i", "127.0.0.1", "localHost: Cloak listens to proxy clients on this ip") 51 | flag.StringVar(&localPort, "l", "1984", "localPort: Cloak listens to proxy clients on this port") 52 | flag.StringVar(&remoteHost, "s", "", "remoteHost: IP of your proxy server") 53 | flag.StringVar(&remotePort, "p", "443", "remotePort: proxy port, should be 443") 54 | flag.BoolVar(&udp, "u", false, "udp: set this flag if the underlying proxy is using UDP protocol") 55 | flag.StringVar(&config, "c", "ckclient.json", "config: path to the configuration file or options separated with semicolons") 56 | flag.StringVar(&proxyMethod, "proxy", "", "proxy: the proxy method's name. It must match exactly with the corresponding entry in server's ProxyBook") 57 | flag.StringVar(&b64AdminUID, "a", "", "adminUID: enter the adminUID to serve the admin api") 58 | askVersion := flag.Bool("v", false, "Print the version number") 59 | printUsage := flag.Bool("h", false, "Print this message") 60 | 61 | // commandline arguments overrides json 62 | 63 | flag.Parse() 64 | 65 | if *askVersion { 66 | fmt.Printf("ck-client %s", version) 67 | return 68 | } 69 | 70 | if *printUsage { 71 | flag.Usage() 72 | return 73 | } 74 | 75 | log.Info("Starting standalone mode") 76 | } 77 | 78 | log.SetFormatter(&log.TextFormatter{ 79 | FullTimestamp: true, 80 | }) 81 | lvl, err := log.ParseLevel(*verbosity) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | log.SetLevel(lvl) 86 | 87 | rawConfig, err := client.ParseConfig(config) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | if ssPluginMode { 93 | if rawConfig.ProxyMethod == "" { 94 | rawConfig.ProxyMethod = "shadowsocks" 95 | } 96 | // json takes precedence over environment variables 97 | // i.e. if json field isn't empty, use that 98 | if rawConfig.RemoteHost == "" { 99 | rawConfig.RemoteHost = os.Getenv("SS_REMOTE_HOST") 100 | } 101 | if rawConfig.RemotePort == "" { 102 | rawConfig.RemotePort = os.Getenv("SS_REMOTE_PORT") 103 | } 104 | if rawConfig.LocalHost == "" { 105 | rawConfig.LocalHost = os.Getenv("SS_LOCAL_HOST") 106 | } 107 | if rawConfig.LocalPort == "" { 108 | rawConfig.LocalPort = os.Getenv("SS_LOCAL_PORT") 109 | } 110 | } else { 111 | // commandline argument takes precedence over json 112 | // if commandline argument is set, use commandline 113 | flag.Visit(func(f *flag.Flag) { 114 | // manually set ones 115 | switch f.Name { 116 | case "i": 117 | rawConfig.LocalHost = localHost 118 | case "l": 119 | rawConfig.LocalPort = localPort 120 | case "s": 121 | rawConfig.RemoteHost = remoteHost 122 | case "p": 123 | rawConfig.RemotePort = remotePort 124 | case "u": 125 | rawConfig.UDP = udp 126 | case "proxy": 127 | rawConfig.ProxyMethod = proxyMethod 128 | } 129 | }) 130 | // ones with default values 131 | if rawConfig.LocalHost == "" { 132 | rawConfig.LocalHost = localHost 133 | } 134 | if rawConfig.LocalPort == "" { 135 | rawConfig.LocalPort = localPort 136 | } 137 | if rawConfig.RemotePort == "" { 138 | rawConfig.RemotePort = remotePort 139 | } 140 | } 141 | 142 | localConfig, remoteConfig, authInfo, err := rawConfig.ProcessRawConfig(common.RealWorldState) 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | 147 | var adminUID []byte 148 | if b64AdminUID != "" { 149 | adminUID, err = base64.StdEncoding.DecodeString(b64AdminUID) 150 | if err != nil { 151 | log.Fatal(err) 152 | } 153 | } 154 | 155 | var seshMaker func() *mux.Session 156 | 157 | d := &net.Dialer{Control: protector, KeepAlive: remoteConfig.KeepAlive} 158 | 159 | if adminUID != nil { 160 | log.Infof("API base is %v", localConfig.LocalAddr) 161 | authInfo.UID = adminUID 162 | authInfo.SessionId = 0 163 | remoteConfig.NumConn = 1 164 | 165 | seshMaker = func() *mux.Session { 166 | return client.MakeSession(remoteConfig, authInfo, d) 167 | } 168 | } else { 169 | var network string 170 | if authInfo.Unordered { 171 | network = "UDP" 172 | } else { 173 | network = "TCP" 174 | } 175 | log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod) 176 | seshMaker = func() *mux.Session { 177 | authInfo := authInfo // copy the struct because we are overwriting SessionId 178 | 179 | randByte := make([]byte, 1) 180 | common.RandRead(authInfo.WorldState.Rand, randByte) 181 | authInfo.MockDomain = localConfig.MockDomainList[int(randByte[0])%len(localConfig.MockDomainList)] 182 | 183 | // sessionID is usergenerated. There shouldn't be a security concern because the scope of 184 | // sessionID is limited to its UID. 185 | quad := make([]byte, 4) 186 | common.RandRead(authInfo.WorldState.Rand, quad) 187 | authInfo.SessionId = binary.BigEndian.Uint32(quad) 188 | return client.MakeSession(remoteConfig, authInfo, d) 189 | } 190 | } 191 | 192 | if authInfo.Unordered { 193 | acceptor := func() (*net.UDPConn, error) { 194 | udpAddr, _ := net.ResolveUDPAddr("udp", localConfig.LocalAddr) 195 | return net.ListenUDP("udp", udpAddr) 196 | } 197 | 198 | client.RouteUDP(acceptor, localConfig.Timeout, remoteConfig.Singleplex, seshMaker) 199 | } else { 200 | listener, err := net.Listen("tcp", localConfig.LocalAddr) 201 | if err != nil { 202 | log.Fatal(err) 203 | } 204 | client.RouteTCP(listener, localConfig.Timeout, remoteConfig.Singleplex, seshMaker) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /internal/server/state.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/cbeuw/Cloak/internal/common" 15 | "github.com/cbeuw/Cloak/internal/server/usermanager" 16 | ) 17 | 18 | type RawConfig struct { 19 | ProxyBook map[string][]string 20 | BindAddr []string 21 | BypassUID [][]byte 22 | RedirAddr string 23 | PrivateKey []byte 24 | AdminUID []byte 25 | DatabasePath string 26 | KeepAlive int 27 | CncMode bool 28 | } 29 | 30 | // State type stores the global state of the program 31 | type State struct { 32 | ProxyBook map[string]net.Addr 33 | ProxyDialer common.Dialer 34 | 35 | WorldState common.WorldState 36 | AdminUID []byte 37 | 38 | BypassUID map[[16]byte]struct{} 39 | StaticPv crypto.PrivateKey 40 | 41 | // TODO: this doesn't have to be a net.Addr; resolution is done in Dial automatically 42 | RedirHost net.Addr 43 | RedirPort string 44 | RedirDialer common.Dialer 45 | 46 | usedRandomM sync.RWMutex 47 | UsedRandom map[[32]byte]int64 48 | 49 | Panel *userPanel 50 | } 51 | 52 | func parseRedirAddr(redirAddr string) (net.Addr, string, error) { 53 | var host string 54 | var port string 55 | colonSep := strings.Split(redirAddr, ":") 56 | if len(colonSep) > 1 { 57 | if len(colonSep) == 2 { 58 | // domain or ipv4 with port 59 | host = colonSep[0] 60 | port = colonSep[1] 61 | } else { 62 | if strings.Contains(redirAddr, "[") { 63 | // ipv6 with port 64 | port = colonSep[len(colonSep)-1] 65 | host = strings.TrimSuffix(redirAddr, "]:"+port) 66 | host = strings.TrimPrefix(host, "[") 67 | } else { 68 | // ipv6 without port 69 | host = redirAddr 70 | } 71 | } 72 | } else { 73 | // domain or ipv4 without port 74 | host = redirAddr 75 | } 76 | 77 | redirHost, err := net.ResolveIPAddr("ip", host) 78 | if err != nil { 79 | return nil, "", fmt.Errorf("unable to resolve RedirAddr: %v. ", err) 80 | } 81 | return redirHost, port, nil 82 | } 83 | 84 | func parseProxyBook(bookEntries map[string][]string) (map[string]net.Addr, error) { 85 | proxyBook := map[string]net.Addr{} 86 | for name, pair := range bookEntries { 87 | name = strings.ToLower(name) 88 | if len(pair) != 2 { 89 | return nil, fmt.Errorf("invalid proxy endpoint and address pair for %v: %v", name, pair) 90 | } 91 | network := strings.ToLower(pair[0]) 92 | switch network { 93 | case "tcp": 94 | addr, err := net.ResolveTCPAddr("tcp", pair[1]) 95 | if err != nil { 96 | return nil, err 97 | } 98 | proxyBook[name] = addr 99 | continue 100 | case "udp": 101 | addr, err := net.ResolveUDPAddr("udp", pair[1]) 102 | if err != nil { 103 | return nil, err 104 | } 105 | proxyBook[name] = addr 106 | continue 107 | } 108 | } 109 | return proxyBook, nil 110 | } 111 | 112 | // ParseConfig reads the config file or semicolon-separated options and parse them into a RawConfig 113 | func ParseConfig(conf string) (raw RawConfig, err error) { 114 | content, errPath := ioutil.ReadFile(conf) 115 | if errPath != nil { 116 | errJson := json.Unmarshal(content, &raw) 117 | if errJson != nil { 118 | err = fmt.Errorf("failed to read/unmarshal configuration, path is invalid or %v", errJson) 119 | return 120 | } 121 | } else { 122 | errJson := json.Unmarshal(content, &raw) 123 | if errJson != nil { 124 | err = fmt.Errorf("failed to read configuration file: %v", errJson) 125 | return 126 | } 127 | } 128 | if raw.ProxyBook == nil { 129 | raw.ProxyBook = make(map[string][]string) 130 | } 131 | return 132 | } 133 | 134 | // InitState process the RawConfig and initialises a server State accordingly 135 | func InitState(preParse RawConfig, worldState common.WorldState) (sta *State, err error) { 136 | sta = &State{ 137 | BypassUID: make(map[[16]byte]struct{}), 138 | ProxyBook: map[string]net.Addr{}, 139 | UsedRandom: map[[32]byte]int64{}, 140 | RedirDialer: &net.Dialer{}, 141 | WorldState: worldState, 142 | } 143 | if preParse.CncMode { 144 | err = errors.New("command & control mode not implemented") 145 | return 146 | } else { 147 | var manager usermanager.UserManager 148 | if len(preParse.AdminUID) == 0 || preParse.DatabasePath == "" { 149 | manager = &usermanager.Voidmanager{} 150 | } else { 151 | manager, err = usermanager.MakeLocalManager(preParse.DatabasePath, worldState) 152 | if err != nil { 153 | return sta, err 154 | } 155 | } 156 | sta.Panel = MakeUserPanel(manager) 157 | } 158 | 159 | if preParse.KeepAlive <= 0 { 160 | sta.ProxyDialer = &net.Dialer{KeepAlive: -1} 161 | } else { 162 | sta.ProxyDialer = &net.Dialer{KeepAlive: time.Duration(preParse.KeepAlive) * time.Second} 163 | } 164 | 165 | sta.RedirHost, sta.RedirPort, err = parseRedirAddr(preParse.RedirAddr) 166 | if err != nil { 167 | err = fmt.Errorf("unable to parse RedirAddr: %v", err) 168 | return 169 | } 170 | 171 | sta.ProxyBook, err = parseProxyBook(preParse.ProxyBook) 172 | if err != nil { 173 | err = fmt.Errorf("unable to parse ProxyBook: %v", err) 174 | return 175 | } 176 | 177 | if len(preParse.PrivateKey) == 0 { 178 | err = fmt.Errorf("must have a valid private key. Run `ck-server -key` to generate one") 179 | return 180 | } 181 | var pv [32]byte 182 | copy(pv[:], preParse.PrivateKey) 183 | sta.StaticPv = &pv 184 | 185 | sta.AdminUID = preParse.AdminUID 186 | 187 | var arrUID [16]byte 188 | for _, UID := range preParse.BypassUID { 189 | copy(arrUID[:], UID) 190 | sta.BypassUID[arrUID] = struct{}{} 191 | } 192 | if len(sta.AdminUID) != 0 { 193 | copy(arrUID[:], sta.AdminUID) 194 | sta.BypassUID[arrUID] = struct{}{} 195 | } 196 | 197 | go sta.UsedRandomCleaner() 198 | return sta, nil 199 | } 200 | 201 | // IsBypass checks if a UID is a bypass user 202 | func (sta *State) IsBypass(UID []byte) bool { 203 | var arrUID [16]byte 204 | copy(arrUID[:], UID) 205 | _, exist := sta.BypassUID[arrUID] 206 | return exist 207 | } 208 | 209 | const timestampTolerance = 180 * time.Second 210 | 211 | const replayCacheAgeLimit = 12 * time.Hour 212 | 213 | // UsedRandomCleaner clears the cache of used random fields every replayCacheAgeLimit 214 | func (sta *State) UsedRandomCleaner() { 215 | for { 216 | time.Sleep(replayCacheAgeLimit) 217 | sta.usedRandomM.Lock() 218 | for key, t := range sta.UsedRandom { 219 | if time.Unix(t, 0).Before(sta.WorldState.Now().Add(timestampTolerance)) { 220 | delete(sta.UsedRandom, key) 221 | } 222 | } 223 | sta.usedRandomM.Unlock() 224 | } 225 | } 226 | 227 | func (sta *State) registerRandom(r [32]byte) bool { 228 | sta.usedRandomM.Lock() 229 | _, used := sta.UsedRandom[r] 230 | sta.UsedRandom[r] = sta.WorldState.Now().Unix() 231 | sta.usedRandomM.Unlock() 232 | return used 233 | } 234 | -------------------------------------------------------------------------------- /internal/multiplex/stream.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | "time" 8 | 9 | "sync" 10 | "sync/atomic" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var ErrBrokenStream = errors.New("broken stream") 16 | 17 | // Stream implements net.Conn. It represents an optionally-ordered, full-duplex, self-contained connection. 18 | // If the session it belongs to runs in ordered mode, it provides ordering guarantee regardless of the underlying 19 | // connection used. 20 | // If the underlying connections the session uses are reliable, Stream is reliable. If they are not, Stream does not 21 | // guarantee reliability. 22 | type Stream struct { 23 | id uint32 24 | 25 | session *Session 26 | 27 | // a buffer (implemented as an asynchronous buffered pipe) to put data we've received from recvFrame but hasn't 28 | // been read by the consumer through Read or WriteTo. 29 | recvBuf recvBuffer 30 | 31 | writingM sync.Mutex 32 | writingFrame Frame // we do the allocation here to save repeated allocations in Write and ReadFrom 33 | 34 | // atomic 35 | closed uint32 36 | 37 | // When we want order guarantee (i.e. session.Unordered is false), 38 | // we assign each stream a fixed underlying connection. 39 | // If the underlying connections the session uses provide ordering guarantee (most likely TCP), 40 | // recvBuffer (implemented by streamBuffer under ordered mode) will not receive out-of-order packets 41 | // so it won't have to use its priority queue to sort it. 42 | // This is not used in unordered connection mode 43 | assignedConn net.Conn 44 | 45 | readFromTimeout time.Duration 46 | } 47 | 48 | func makeStream(sesh *Session, id uint32) *Stream { 49 | stream := &Stream{ 50 | id: id, 51 | session: sesh, 52 | writingFrame: Frame{ 53 | StreamID: id, 54 | Seq: 0, 55 | Closing: closingNothing, 56 | }, 57 | } 58 | 59 | if sesh.Unordered { 60 | stream.recvBuf = NewDatagramBufferedPipe() 61 | } else { 62 | stream.recvBuf = NewStreamBuffer() 63 | } 64 | 65 | return stream 66 | } 67 | 68 | func (s *Stream) isClosed() bool { return atomic.LoadUint32(&s.closed) == 1 } 69 | 70 | // receive a readily deobfuscated Frame so its payload can later be Read 71 | func (s *Stream) recvFrame(frame *Frame) error { 72 | toBeClosed, err := s.recvBuf.Write(frame) 73 | if toBeClosed { 74 | err = s.passiveClose() 75 | if errors.Is(err, errRepeatStreamClosing) { 76 | log.Debug(err) 77 | return nil 78 | } 79 | return err 80 | } 81 | return err 82 | } 83 | 84 | // Read implements io.Read 85 | func (s *Stream) Read(buf []byte) (n int, err error) { 86 | //log.Tracef("attempting to read from stream %v", s.id) 87 | if len(buf) == 0 { 88 | return 0, nil 89 | } 90 | 91 | n, err = s.recvBuf.Read(buf) 92 | log.Tracef("%v read from stream %v with err %v", n, s.id, err) 93 | if err == io.EOF { 94 | return n, ErrBrokenStream 95 | } 96 | return 97 | } 98 | 99 | func (s *Stream) obfuscateAndSend(buf []byte, payloadOffsetInBuf int) error { 100 | cipherTextLen, err := s.session.obfuscate(&s.writingFrame, buf, payloadOffsetInBuf) 101 | s.writingFrame.Seq++ 102 | if err != nil { 103 | return err 104 | } 105 | 106 | _, err = s.session.sb.send(buf[:cipherTextLen], &s.assignedConn) 107 | if err != nil { 108 | if err == errBrokenSwitchboard { 109 | s.session.SetTerminalMsg(err.Error()) 110 | s.session.passiveClose() 111 | } 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | // Write implements io.Write 118 | func (s *Stream) Write(in []byte) (n int, err error) { 119 | s.writingM.Lock() 120 | defer s.writingM.Unlock() 121 | if s.isClosed() { 122 | return 0, ErrBrokenStream 123 | } 124 | 125 | for n < len(in) { 126 | var framePayload []byte 127 | if len(in)-n <= s.session.maxStreamUnitWrite { 128 | // if we can fit remaining data of in into one frame 129 | framePayload = in[n:] 130 | } else { 131 | // if we have to split 132 | if s.session.Unordered { 133 | // but we are not allowed to 134 | err = io.ErrShortBuffer 135 | return 136 | } 137 | framePayload = in[n : s.session.maxStreamUnitWrite+n] 138 | } 139 | s.writingFrame.Payload = framePayload 140 | buf := s.session.streamObfsBufPool.Get().(*[]byte) 141 | err = s.obfuscateAndSend(*buf, 0) 142 | s.session.streamObfsBufPool.Put(buf) 143 | if err != nil { 144 | return 145 | } 146 | n += len(framePayload) 147 | } 148 | return 149 | } 150 | 151 | // ReadFrom continuously read data from r and send it off, until either r returns error or nothing has been read 152 | // for readFromTimeout amount of time 153 | func (s *Stream) ReadFrom(r io.Reader) (n int64, err error) { 154 | for { 155 | if s.readFromTimeout != 0 { 156 | if rder, ok := r.(net.Conn); !ok { 157 | log.Warn("ReadFrom timeout is set but reader doesn't implement SetReadDeadline") 158 | } else { 159 | rder.SetReadDeadline(time.Now().Add(s.readFromTimeout)) 160 | } 161 | } 162 | buf := s.session.streamObfsBufPool.Get().(*[]byte) 163 | read, er := r.Read((*buf)[frameHeaderLength : frameHeaderLength+s.session.maxStreamUnitWrite]) 164 | if er != nil { 165 | return n, er 166 | } 167 | 168 | // the above read may have been unblocked by another goroutine calling stream.Close(), so we need 169 | // to check that here 170 | if s.isClosed() { 171 | return n, ErrBrokenStream 172 | } 173 | 174 | s.writingM.Lock() 175 | s.writingFrame.Payload = (*buf)[frameHeaderLength : frameHeaderLength+read] 176 | err = s.obfuscateAndSend(*buf, frameHeaderLength) 177 | s.writingM.Unlock() 178 | s.session.streamObfsBufPool.Put(buf) 179 | 180 | if err != nil { 181 | return 182 | } 183 | n += int64(read) 184 | } 185 | } 186 | 187 | func (s *Stream) passiveClose() error { 188 | return s.session.closeStream(s, false) 189 | } 190 | 191 | // active close. Close locally and tell the remote that this stream is being closed 192 | func (s *Stream) Close() error { 193 | s.writingM.Lock() 194 | defer s.writingM.Unlock() 195 | 196 | return s.session.closeStream(s, true) 197 | } 198 | 199 | func (s *Stream) LocalAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[0] } 200 | func (s *Stream) RemoteAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[1] } 201 | 202 | func (s *Stream) SetReadDeadline(t time.Time) error { s.recvBuf.SetReadDeadline(t); return nil } 203 | func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d } 204 | 205 | var errNotImplemented = errors.New("Not implemented") 206 | 207 | // the following functions are purely for implementing net.Conn interface. 208 | // they are not used 209 | // TODO: implement the following 210 | func (s *Stream) SetDeadline(t time.Time) error { return errNotImplemented } 211 | func (s *Stream) SetWriteDeadline(t time.Time) error { return errNotImplemented } 212 | -------------------------------------------------------------------------------- /internal/server/websocketAux.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/cbeuw/Cloak/internal/common" 9 | "github.com/gorilla/websocket" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // The code in this file is mostly to obtain a binary-oriented, net.Conn analogous 15 | // util.WebSocketConn from the awkward APIs of gorilla/websocket and net/http 16 | // 17 | // The flow of our process is: accept a Conn from remote, read the first packet remote sent us. If it's in the format 18 | // of a TLS handshake, we hand it over to the TLS part; if it's in the format of a HTTP request, we process it as a 19 | // websocket and eventually wrap the remote Conn as util.WebSocketConn, 20 | // 21 | // To get a util.WebSocketConn, we need a gorilla/websocket.Conn. This is obtained by using upgrader.Upgrade method 22 | // inside a HTTP request handler function (which is defined by us). The HTTP request handler function is invoked by 23 | // net/http package upon receiving a request from a Conn. 24 | // 25 | // Ideally we want to give net/http the connection we got from remote, then it can read the first packet (which should 26 | // be an HTTP request) from that Conn and call the handler function, which can then be upgraded to obtain a 27 | // gorilla/websocket.Conn. But this won't work for two reasons: one is that we have ALREADY READ the request packet 28 | // from the remote Conn to determine if it's TLS or HTTP. When net/http reads from the Conn, it will not receive that 29 | // request packet. The second reason is that there is no API in net/http that accepts a Conn at all. Instead, the 30 | // closest we can get is http.Serve which takes in a net.Listener and a http.Handler which implements the ServeHTTP 31 | // function. 32 | // 33 | // Recall that net.Listener has a method Accept which blocks until the Listener receives a connection, then 34 | // it returns a net.Conn. net/http calls Listener.Accept repeatedly and creates a new goroutine handling each Conn 35 | // accepted. 36 | // 37 | // So here is what we need to do: we need to create a type WsAcceptor that implements net.Listener interface. 38 | // the first time WsAcceptor.Accept is called, it will return something that implements net.Conn, subsequent calls to 39 | // Accept will return error (so that the caller won't call again) 40 | // 41 | // The "something that implements net.Conn" needs to do the following: the first time Read is called, it returns the 42 | // request packet we got from the remote Conn which we have already read, so that the packet, which is an HTTP request 43 | // will be processed by the handling function. Subsequent calls to Read will read directly from the remote Conn. To do 44 | // this we create a type firstBuffedConn that implements net.Conn. When we instantiate a firstBuffedConn object, we 45 | // give it the request packet we have already read from the remote Conn, as well as the reference to the remote Conn. 46 | // 47 | // So now we call http.Serve(WsAcceptor, [some handler]), net/http will call WsAcceptor.Accept, which returns a 48 | // firstBuffedConn. net/http will call WsAcceptor.Accept again but this time it returns error so net/http will stop. 49 | // firstBuffedConn.Read will then be called, which returns the request packet from remote Conn. Then 50 | // [some handler].ServeHTTP will be called, in which websocket.upgrader.Upgrade will be called to obtain a 51 | // websocket.Conn 52 | // 53 | // One problem remains: websocket.upgrader.Upgrade is called inside the handling function. The websocket.Conn it 54 | // returned needs to be somehow preserved so we can keep using it. To do this, we define a type WsHandshakeHandler 55 | // which implements http.Handler. WsHandshakeHandler has a struct field of type net.Conn that can be set. Inside 56 | // WsHandshakeHandler.ServeHTTP, the returned websocket.Conn from upgrader.Upgrade will be converted into a 57 | // util.WebSocketConn, whose reference will be kept in the struct field. Whoever has the reference to the instance of 58 | // WsHandshakeHandler can get the reference to the established util.WebSocketConn. 59 | // 60 | // There is another problem: the call of http.Serve(WsAcceptor, WsHandshakeHandler) is async. We don't know when 61 | // the instance of WsHandshakeHandler will have the util.WebSocketConn ready. We synchronise this using a channel. 62 | // A channel called finished will be provided to an instance of WsHandshakeHandler upon its creation. Once 63 | // WsHandshakeHandler.ServeHTTP has the reference to util.WebSocketConn ready, it will write to finished. 64 | // Outside, immediately after the call to http.Serve(WsAcceptor, WsHandshakeHandler), we read from finished so that the 65 | // execution will block until the reference to util.WebSocketConn is ready. 66 | 67 | // since we need to read the first packet from the client to identify its protocol, the first packet will no longer 68 | // be in Conn's buffer. However, websocket.Upgrade relies on reading the first packet for handshake, so we must 69 | // fake a conn that returns the first packet on first read 70 | type firstBuffedConn struct { 71 | net.Conn 72 | firstRead bool 73 | firstPacket []byte 74 | } 75 | 76 | func (c *firstBuffedConn) Read(buf []byte) (int, error) { 77 | if !c.firstRead { 78 | c.firstRead = true 79 | copy(buf, c.firstPacket) 80 | n := len(c.firstPacket) 81 | c.firstPacket = []byte{} 82 | return n, nil 83 | } 84 | return c.Conn.Read(buf) 85 | } 86 | 87 | type wsOnceListener struct { 88 | done bool 89 | c *firstBuffedConn 90 | } 91 | 92 | // net/http provides no method to serve an existing connection, we must feed in a net.Accept interface to get an 93 | // http.Server. This is an acceptor that accepts only one Conn 94 | func newWsAcceptor(conn net.Conn, first []byte) *wsOnceListener { 95 | f := make([]byte, len(first)) 96 | copy(f, first) 97 | return &wsOnceListener{ 98 | c: &firstBuffedConn{Conn: conn, firstPacket: f}, 99 | } 100 | } 101 | 102 | func (w *wsOnceListener) Accept() (net.Conn, error) { 103 | if w.done { 104 | return nil, errors.New("already accepted") 105 | } 106 | w.done = true 107 | return w.c, nil 108 | } 109 | 110 | func (w *wsOnceListener) Close() error { 111 | w.done = true 112 | return nil 113 | } 114 | 115 | func (w *wsOnceListener) Addr() net.Addr { 116 | return w.c.LocalAddr() 117 | } 118 | 119 | type wsHandshakeHandler struct { 120 | conn net.Conn 121 | finished chan struct{} 122 | } 123 | 124 | // the handler to turn a net.Conn into a websocket.Conn 125 | func newWsHandshakeHandler() *wsHandshakeHandler { 126 | return &wsHandshakeHandler{finished: make(chan struct{})} 127 | } 128 | 129 | func (ws *wsHandshakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 130 | upgrader := websocket.Upgrader{} 131 | c, err := upgrader.Upgrade(w, r, nil) 132 | if err != nil { 133 | log.Errorf("failed to upgrade connection to ws: %v", err) 134 | return 135 | } 136 | ws.conn = &common.WebSocketConn{Conn: c} 137 | ws.finished <- struct{}{} 138 | } 139 | -------------------------------------------------------------------------------- /internal/server/usermanager/api_router_test.go: -------------------------------------------------------------------------------- 1 | package usermanager 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | var mockUIDb64 = base64.URLEncoding.EncodeToString(mockUID) 17 | 18 | func makeRouter(t *testing.T) (router *APIRouter, cleaner func()) { 19 | var tmpDB, _ = ioutil.TempFile("", "ck_user_info") 20 | cleaner = func() { os.Remove(tmpDB.Name()) } 21 | mgr, err := MakeLocalManager(tmpDB.Name(), mockWorldState) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | router = APIRouterOf(mgr) 26 | return router, cleaner 27 | } 28 | 29 | func TestWriteUserInfoHlr(t *testing.T) { 30 | router, cleaner := makeRouter(t) 31 | defer cleaner() 32 | 33 | marshalled, err := json.Marshal(mockUserInfo) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | t.Run("ok", func(t *testing.T) { 39 | req, err := http.NewRequest("POST", "/admin/users/"+mockUIDb64, bytes.NewBuffer(marshalled)) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | rr := httptest.NewRecorder() 45 | router.ServeHTTP(rr, req) 46 | 47 | assert.Equalf(t, http.StatusCreated, rr.Code, "response body: %v", rr.Body) 48 | }) 49 | 50 | t.Run("partial update", func(t *testing.T) { 51 | req, err := http.NewRequest("POST", "/admin/users/"+mockUIDb64, bytes.NewBuffer(marshalled)) 52 | assert.NoError(t, err) 53 | rr := httptest.NewRecorder() 54 | router.ServeHTTP(rr, req) 55 | assert.Equal(t, http.StatusCreated, rr.Code) 56 | 57 | partialUserInfo := UserInfo{ 58 | UID: mockUID, 59 | SessionsCap: JustInt32(10), 60 | } 61 | partialMarshalled, _ := json.Marshal(partialUserInfo) 62 | req, err = http.NewRequest("POST", "/admin/users/"+mockUIDb64, bytes.NewBuffer(partialMarshalled)) 63 | assert.NoError(t, err) 64 | router.ServeHTTP(rr, req) 65 | assert.Equal(t, http.StatusCreated, rr.Code) 66 | 67 | req, err = http.NewRequest("GET", "/admin/users/"+mockUIDb64, nil) 68 | assert.NoError(t, err) 69 | router.ServeHTTP(rr, req) 70 | assert.Equal(t, http.StatusCreated, rr.Code) 71 | var got UserInfo 72 | err = json.Unmarshal(rr.Body.Bytes(), &got) 73 | assert.NoError(t, err) 74 | 75 | expected := mockUserInfo 76 | expected.SessionsCap = partialUserInfo.SessionsCap 77 | assert.EqualValues(t, expected, got) 78 | }) 79 | 80 | t.Run("empty parameter", func(t *testing.T) { 81 | req, err := http.NewRequest("POST", "/admin/users/", bytes.NewBuffer(marshalled)) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | rr := httptest.NewRecorder() 86 | router.ServeHTTP(rr, req) 87 | 88 | assert.Equalf(t, http.StatusMethodNotAllowed, rr.Code, "response body: %v", rr.Body) 89 | }) 90 | 91 | t.Run("UID mismatch", func(t *testing.T) { 92 | badMock := mockUserInfo 93 | badMock.UID = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0} 94 | badMarshal, err := json.Marshal(badMock) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | req, err := http.NewRequest("POST", "/admin/users/"+mockUIDb64, bytes.NewBuffer(badMarshal)) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | 103 | rr := httptest.NewRecorder() 104 | router.ServeHTTP(rr, req) 105 | 106 | assert.Equalf(t, http.StatusBadRequest, rr.Code, "response body: %v", rr.Body) 107 | }) 108 | 109 | t.Run("garbage data", func(t *testing.T) { 110 | req, err := http.NewRequest("POST", "/admin/users/"+mockUIDb64, bytes.NewBuffer([]byte(`{"{{'{;;}}}1`))) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | rr := httptest.NewRecorder() 116 | router.ServeHTTP(rr, req) 117 | 118 | assert.Equalf(t, http.StatusBadRequest, rr.Code, "response body: %v", rr.Body) 119 | }) 120 | 121 | t.Run("not base64", func(t *testing.T) { 122 | req, err := http.NewRequest("POST", "/admin/users/"+"defonotbase64", bytes.NewBuffer(marshalled)) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | rr := httptest.NewRecorder() 128 | router.ServeHTTP(rr, req) 129 | 130 | assert.Equalf(t, http.StatusBadRequest, rr.Code, "response body: %v", rr.Body) 131 | }) 132 | } 133 | 134 | func addUser(t *testing.T, router *APIRouter, user UserInfo) { 135 | marshalled, err := json.Marshal(user) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | req, err := http.NewRequest("POST", "/admin/users/"+base64.URLEncoding.EncodeToString(user.UID), bytes.NewBuffer(marshalled)) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | rr := httptest.NewRecorder() 144 | router.ServeHTTP(rr, req) 145 | assert.Equalf(t, http.StatusCreated, rr.Code, "response body: %v", rr.Body) 146 | } 147 | 148 | func TestGetUserInfoHlr(t *testing.T) { 149 | router, cleaner := makeRouter(t) 150 | defer cleaner() 151 | 152 | t.Run("empty parameter", func(t *testing.T) { 153 | assert.HTTPError(t, router.ServeHTTP, "GET", "/admin/users/", nil) 154 | }) 155 | 156 | t.Run("non-existent", func(t *testing.T) { 157 | assert.HTTPError(t, router.ServeHTTP, "GET", "/admin/users/"+base64.URLEncoding.EncodeToString([]byte("adsf")), nil) 158 | }) 159 | 160 | t.Run("not base64", func(t *testing.T) { 161 | assert.HTTPError(t, router.ServeHTTP, "GET", "/admin/users/"+"defonotbase64", nil) 162 | }) 163 | 164 | t.Run("ok", func(t *testing.T) { 165 | addUser(t, router, mockUserInfo) 166 | 167 | var got UserInfo 168 | err := json.Unmarshal([]byte(assert.HTTPBody(router.ServeHTTP, "GET", "/admin/users/"+mockUIDb64, nil)), &got) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | assert.EqualValues(t, mockUserInfo, got) 173 | }) 174 | } 175 | 176 | func TestDeleteUserHlr(t *testing.T) { 177 | router, cleaner := makeRouter(t) 178 | defer cleaner() 179 | 180 | t.Run("non-existent", func(t *testing.T) { 181 | assert.HTTPError(t, router.ServeHTTP, "DELETE", "/admin/users/"+base64.URLEncoding.EncodeToString([]byte("adsf")), nil) 182 | }) 183 | 184 | t.Run("not base64", func(t *testing.T) { 185 | assert.HTTPError(t, router.ServeHTTP, "DELETE", "/admin/users/"+"defonotbase64", nil) 186 | }) 187 | 188 | t.Run("ok", func(t *testing.T) { 189 | addUser(t, router, mockUserInfo) 190 | assert.HTTPSuccess(t, router.ServeHTTP, "DELETE", "/admin/users/"+mockUIDb64, nil) 191 | assert.HTTPError(t, router.ServeHTTP, "GET", "/admin/users/"+mockUIDb64, nil) 192 | }) 193 | } 194 | 195 | func TestListAllUsersHlr(t *testing.T) { 196 | router, cleaner := makeRouter(t) 197 | defer cleaner() 198 | 199 | user1 := mockUserInfo 200 | addUser(t, router, user1) 201 | 202 | user2 := mockUserInfo 203 | user2.UID = []byte{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2} 204 | addUser(t, router, user2) 205 | 206 | expected := []UserInfo{user1, user2} 207 | 208 | var got []UserInfo 209 | err := json.Unmarshal([]byte(assert.HTTPBody(router.ServeHTTP, "GET", "/admin/users", nil)), &got) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | assert.True(t, assert.Subset(t, got, expected), assert.Subset(t, expected, got)) 214 | } 215 | -------------------------------------------------------------------------------- /internal/server/TLSAux.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/cbeuw/Cloak/internal/common" 10 | ) 11 | 12 | // ClientHello contains every field in a ClientHello message 13 | type ClientHello struct { 14 | handshakeType byte 15 | length int 16 | clientVersion []byte 17 | random []byte 18 | sessionIdLen int 19 | sessionId []byte 20 | cipherSuitesLen int 21 | cipherSuites []byte 22 | compressionMethodsLen int 23 | compressionMethods []byte 24 | extensionsLen int 25 | extensions map[[2]byte][]byte 26 | } 27 | 28 | var u16 = binary.BigEndian.Uint16 29 | var u32 = binary.BigEndian.Uint32 30 | 31 | func parseExtensions(input []byte) (ret map[[2]byte][]byte, err error) { 32 | defer func() { 33 | if r := recover(); r != nil { 34 | err = errors.New("Malformed Extensions") 35 | } 36 | }() 37 | pointer := 0 38 | totalLen := len(input) 39 | ret = make(map[[2]byte][]byte) 40 | for pointer < totalLen { 41 | var typ [2]byte 42 | copy(typ[:], input[pointer:pointer+2]) 43 | pointer += 2 44 | length := int(u16(input[pointer : pointer+2])) 45 | pointer += 2 46 | data := input[pointer : pointer+length] 47 | pointer += length 48 | ret[typ] = data 49 | } 50 | return ret, err 51 | } 52 | 53 | func parseKeyShare(input []byte) (ret []byte, err error) { 54 | defer func() { 55 | if r := recover(); r != nil { 56 | err = errors.New("malformed key_share") 57 | } 58 | }() 59 | totalLen := int(u16(input[0:2])) 60 | // 2 bytes "client key share length" 61 | pointer := 2 62 | for pointer < totalLen { 63 | if bytes.Equal([]byte{0x00, 0x1d}, input[pointer:pointer+2]) { 64 | // skip "key exchange length" 65 | pointer += 2 66 | length := int(u16(input[pointer : pointer+2])) 67 | pointer += 2 68 | if length != 32 { 69 | return nil, fmt.Errorf("key share length should be 32, instead of %v", length) 70 | } 71 | return input[pointer : pointer+length], nil 72 | } 73 | pointer += 2 74 | length := int(u16(input[pointer : pointer+2])) 75 | pointer += 2 76 | _ = input[pointer : pointer+length] 77 | pointer += length 78 | } 79 | return nil, errors.New("x25519 does not exist") 80 | } 81 | 82 | // addRecordLayer adds record layer to data 83 | func addRecordLayer(input []byte, typ []byte, ver []byte) []byte { 84 | length := make([]byte, 2) 85 | binary.BigEndian.PutUint16(length, uint16(len(input))) 86 | ret := make([]byte, 5+len(input)) 87 | copy(ret[0:1], typ) 88 | copy(ret[1:3], ver) 89 | copy(ret[3:5], length) 90 | copy(ret[5:], input) 91 | return ret 92 | } 93 | 94 | // parseClientHello parses everything on top of the TLS layer 95 | // (including the record layer) into ClientHello type 96 | func parseClientHello(data []byte) (ret *ClientHello, err error) { 97 | defer func() { 98 | if r := recover(); r != nil { 99 | err = errors.New("Malformed ClientHello") 100 | } 101 | }() 102 | 103 | if !bytes.Equal(data[0:3], []byte{0x16, 0x03, 0x01}) { 104 | return ret, errors.New("wrong TLS1.3 handshake magic bytes") 105 | } 106 | 107 | peeled := make([]byte, len(data)-5) 108 | copy(peeled, data[5:]) 109 | pointer := 0 110 | // Handshake Type 111 | handshakeType := peeled[pointer] 112 | if handshakeType != 0x01 { 113 | return ret, errors.New("Not a ClientHello") 114 | } 115 | pointer += 1 116 | // Length 117 | length := int(u32(append([]byte{0x00}, peeled[pointer:pointer+3]...))) 118 | pointer += 3 119 | if length != len(peeled[pointer:]) { 120 | return ret, errors.New("Hello length doesn't match") 121 | } 122 | // Client Version 123 | clientVersion := peeled[pointer : pointer+2] 124 | pointer += 2 125 | // Random 126 | random := peeled[pointer : pointer+32] 127 | pointer += 32 128 | // Session ID 129 | sessionIdLen := int(peeled[pointer]) 130 | pointer += 1 131 | sessionId := peeled[pointer : pointer+sessionIdLen] 132 | pointer += sessionIdLen 133 | // Cipher Suites 134 | cipherSuitesLen := int(u16(peeled[pointer : pointer+2])) 135 | pointer += 2 136 | cipherSuites := peeled[pointer : pointer+cipherSuitesLen] 137 | pointer += cipherSuitesLen 138 | // Compression Methods 139 | compressionMethodsLen := int(peeled[pointer]) 140 | pointer += 1 141 | compressionMethods := peeled[pointer : pointer+compressionMethodsLen] 142 | pointer += compressionMethodsLen 143 | // Extensions 144 | extensionsLen := int(u16(peeled[pointer : pointer+2])) 145 | pointer += 2 146 | extensions, err := parseExtensions(peeled[pointer:]) 147 | ret = &ClientHello{ 148 | handshakeType, 149 | length, 150 | clientVersion, 151 | random, 152 | sessionIdLen, 153 | sessionId, 154 | cipherSuitesLen, 155 | cipherSuites, 156 | compressionMethodsLen, 157 | compressionMethods, 158 | extensionsLen, 159 | extensions, 160 | } 161 | return 162 | } 163 | 164 | func composeServerHello(sessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte) []byte { 165 | var serverHello [11][]byte 166 | serverHello[0] = []byte{0x02} // handshake type 167 | serverHello[1] = []byte{0x00, 0x00, 0x76} // length 118 168 | serverHello[2] = []byte{0x03, 0x03} // server version 169 | serverHello[3] = append(nonce[0:12], encryptedSessionKeyWithTag[0:20]...) // random 32 bytes 170 | serverHello[4] = []byte{0x20} // session id length 32 171 | serverHello[5] = sessionId // session id 172 | serverHello[6] = []byte{0x13, 0x02} // cipher suite TLS_AES_256_GCM_SHA384 173 | serverHello[7] = []byte{0x00} // compression method null 174 | serverHello[8] = []byte{0x00, 0x2e} // extensions length 46 175 | 176 | keyShare := []byte{0x00, 0x33, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20} 177 | keyExchange := make([]byte, 32) 178 | copy(keyExchange, encryptedSessionKeyWithTag[20:48]) 179 | common.CryptoRandRead(keyExchange[28:32]) 180 | serverHello[9] = append(keyShare, keyExchange...) 181 | 182 | serverHello[10] = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04} // supported versions 183 | var ret []byte 184 | for _, s := range serverHello { 185 | ret = append(ret, s...) 186 | } 187 | return ret 188 | } 189 | 190 | // composeReply composes the ServerHello, ChangeCipherSpec and an ApplicationData messages 191 | // together with their respective record layers into one byte slice. 192 | func composeReply(clientHelloSessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte, cert []byte) []byte { 193 | TLS12 := []byte{0x03, 0x03} 194 | sh := composeServerHello(clientHelloSessionId, nonce, encryptedSessionKeyWithTag) 195 | shBytes := addRecordLayer(sh, []byte{0x16}, TLS12) 196 | ccsBytes := addRecordLayer([]byte{0x01}, []byte{0x14}, TLS12) 197 | 198 | encryptedCertBytes := addRecordLayer(cert, []byte{0x17}, TLS12) 199 | ret := append(shBytes, ccsBytes...) 200 | ret = append(ret, encryptedCertBytes...) 201 | return ret 202 | } 203 | -------------------------------------------------------------------------------- /internal/server/userpanel.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/base64" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/cbeuw/Cloak/internal/server/usermanager" 10 | 11 | mux "github.com/cbeuw/Cloak/internal/multiplex" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | const defaultUploadInterval = 1 * time.Minute 16 | 17 | // userPanel is used to authenticate new users and book keep active users 18 | type userPanel struct { 19 | Manager usermanager.UserManager 20 | 21 | activeUsersM sync.RWMutex 22 | activeUsers map[[16]byte]*ActiveUser 23 | usageUpdateQueueM sync.Mutex 24 | usageUpdateQueue map[[16]byte]*usagePair 25 | 26 | uploadInterval time.Duration 27 | } 28 | 29 | func MakeUserPanel(manager usermanager.UserManager) *userPanel { 30 | ret := &userPanel{ 31 | Manager: manager, 32 | activeUsers: make(map[[16]byte]*ActiveUser), 33 | usageUpdateQueue: make(map[[16]byte]*usagePair), 34 | uploadInterval: defaultUploadInterval, 35 | } 36 | go ret.regularQueueUpload() 37 | return ret 38 | } 39 | 40 | // GetBypassUser does the same as GetUser except it unconditionally creates an ActiveUser when the UID isn't already active 41 | func (panel *userPanel) GetBypassUser(UID []byte) (*ActiveUser, error) { 42 | panel.activeUsersM.Lock() 43 | defer panel.activeUsersM.Unlock() 44 | var arrUID [16]byte 45 | copy(arrUID[:], UID) 46 | if user, ok := panel.activeUsers[arrUID]; ok { 47 | return user, nil 48 | } 49 | user := &ActiveUser{ 50 | panel: panel, 51 | valve: mux.UNLIMITED_VALVE, 52 | sessions: make(map[uint32]*mux.Session), 53 | bypass: true, 54 | } 55 | copy(user.arrUID[:], UID) 56 | panel.activeUsers[user.arrUID] = user 57 | return user, nil 58 | } 59 | 60 | // GetUser retrieves the reference to an ActiveUser if it's already active, or creates a new ActiveUser of specified 61 | // UID with UserInfo queried from the UserManger, should the particular UID is allowed to connect 62 | func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) { 63 | panel.activeUsersM.Lock() 64 | defer panel.activeUsersM.Unlock() 65 | var arrUID [16]byte 66 | copy(arrUID[:], UID) 67 | if user, ok := panel.activeUsers[arrUID]; ok { 68 | return user, nil 69 | } 70 | 71 | upRate, downRate, err := panel.Manager.AuthenticateUser(UID) 72 | if err != nil { 73 | return nil, err 74 | } 75 | valve := mux.MakeValve(upRate, downRate) 76 | user := &ActiveUser{ 77 | panel: panel, 78 | valve: valve, 79 | sessions: make(map[uint32]*mux.Session), 80 | } 81 | 82 | copy(user.arrUID[:], UID) 83 | panel.activeUsers[user.arrUID] = user 84 | log.WithFields(log.Fields{ 85 | "UID": base64.StdEncoding.EncodeToString(UID), 86 | }).Info("New active user") 87 | return user, nil 88 | } 89 | 90 | // TerminateActiveUser terminates a user and deletes its references 91 | func (panel *userPanel) TerminateActiveUser(user *ActiveUser, reason string) { 92 | log.WithFields(log.Fields{ 93 | "UID": base64.StdEncoding.EncodeToString(user.arrUID[:]), 94 | "reason": reason, 95 | }).Info("Terminating active user") 96 | panel.updateUsageQueueForOne(user) 97 | user.closeAllSessions(reason) 98 | panel.activeUsersM.Lock() 99 | delete(panel.activeUsers, user.arrUID) 100 | panel.activeUsersM.Unlock() 101 | } 102 | 103 | func (panel *userPanel) isActive(UID []byte) bool { 104 | var arrUID [16]byte 105 | copy(arrUID[:], UID) 106 | panel.activeUsersM.RLock() 107 | _, ok := panel.activeUsers[arrUID] 108 | panel.activeUsersM.RUnlock() 109 | return ok 110 | } 111 | 112 | type usagePair struct { 113 | up *int64 114 | down *int64 115 | } 116 | 117 | // updateUsageQueue zeroes the accumulated usage all ActiveUsers valve and put the usage data im usageUpdateQueue 118 | func (panel *userPanel) updateUsageQueue() { 119 | panel.activeUsersM.Lock() 120 | panel.usageUpdateQueueM.Lock() 121 | for _, user := range panel.activeUsers { 122 | if user.bypass { 123 | continue 124 | } 125 | 126 | upIncured, downIncured := user.valve.Nullify() 127 | if usage, ok := panel.usageUpdateQueue[user.arrUID]; ok { 128 | atomic.AddInt64(usage.up, upIncured) 129 | atomic.AddInt64(usage.down, downIncured) 130 | } else { 131 | // if the user hasn't been added to the queue 132 | usage = &usagePair{&upIncured, &downIncured} 133 | panel.usageUpdateQueue[user.arrUID] = usage 134 | } 135 | } 136 | panel.activeUsersM.Unlock() 137 | panel.usageUpdateQueueM.Unlock() 138 | } 139 | 140 | // updateUsageQueueForOne is the same as updateUsageQueue except it only updates one user's usage 141 | // this is useful when the user is being terminated 142 | func (panel *userPanel) updateUsageQueueForOne(user *ActiveUser) { 143 | // used when one particular user deactivates 144 | if user.bypass { 145 | return 146 | } 147 | upIncured, downIncured := user.valve.Nullify() 148 | panel.usageUpdateQueueM.Lock() 149 | if usage, ok := panel.usageUpdateQueue[user.arrUID]; ok { 150 | atomic.AddInt64(usage.up, upIncured) 151 | atomic.AddInt64(usage.down, downIncured) 152 | } else { 153 | usage = &usagePair{&upIncured, &downIncured} 154 | panel.usageUpdateQueue[user.arrUID] = usage 155 | } 156 | panel.usageUpdateQueueM.Unlock() 157 | 158 | } 159 | 160 | // commitUpdate put all usageUpdates into a slice of StatusUpdate, calls Manager.UploadStatus, gets the responses 161 | // and act to each user according to the responses 162 | func (panel *userPanel) commitUpdate() error { 163 | panel.usageUpdateQueueM.Lock() 164 | statuses := make([]usermanager.StatusUpdate, 0, len(panel.usageUpdateQueue)) 165 | for arrUID, usage := range panel.usageUpdateQueue { 166 | panel.activeUsersM.RLock() 167 | user := panel.activeUsers[arrUID] 168 | panel.activeUsersM.RUnlock() 169 | var numSession int 170 | if user != nil { 171 | if user.bypass { 172 | continue 173 | } 174 | numSession = user.NumSession() 175 | } 176 | status := usermanager.StatusUpdate{ 177 | UID: arrUID[:], 178 | Active: panel.isActive(arrUID[:]), 179 | NumSession: numSession, 180 | UpUsage: *usage.up, 181 | DownUsage: *usage.down, 182 | Timestamp: time.Now().Unix(), 183 | } 184 | statuses = append(statuses, status) 185 | } 186 | panel.usageUpdateQueue = make(map[[16]byte]*usagePair) 187 | panel.usageUpdateQueueM.Unlock() 188 | 189 | if len(statuses) == 0 { 190 | return nil 191 | } 192 | responses, err := panel.Manager.UploadStatus(statuses) 193 | if err != nil { 194 | return err 195 | } 196 | for _, resp := range responses { 197 | var arrUID [16]byte 198 | copy(arrUID[:], resp.UID) 199 | switch resp.Action { 200 | case usermanager.TERMINATE: 201 | panel.activeUsersM.RLock() 202 | user := panel.activeUsers[arrUID] 203 | panel.activeUsersM.RUnlock() 204 | if user != nil { 205 | panel.TerminateActiveUser(user, resp.Message) 206 | } 207 | } 208 | } 209 | return nil 210 | } 211 | 212 | func (panel *userPanel) regularQueueUpload() { 213 | for { 214 | time.Sleep(panel.uploadInterval) 215 | go func() { 216 | panel.updateUsageQueue() 217 | err := panel.commitUpdate() 218 | if err != nil { 219 | log.Error(err) 220 | } 221 | }() 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /internal/multiplex/obfs.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "github.com/cbeuw/Cloak/internal/common" 11 | "golang.org/x/crypto/chacha20poly1305" 12 | "golang.org/x/crypto/salsa20" 13 | ) 14 | 15 | const frameHeaderLength = 14 16 | const salsa20NonceSize = 8 17 | 18 | // maxExtraLen equals the max length of padding + AEAD tag. 19 | // It is 255 bytes because the extra len field in frame header is only one byte. 20 | const maxExtraLen = 1<<8 - 1 21 | 22 | // padFirstNFrames specifies the number of initial frames to pad, 23 | // to avoid TLS-in-TLS detection 24 | const padFirstNFrames = 5 25 | 26 | const ( 27 | EncryptionMethodPlain = iota 28 | EncryptionMethodAES256GCM 29 | EncryptionMethodChaha20Poly1305 30 | EncryptionMethodAES128GCM 31 | ) 32 | 33 | // Obfuscator is responsible for serialisation, obfuscation, and optional encryption of data frames. 34 | type Obfuscator struct { 35 | payloadCipher cipher.AEAD 36 | 37 | sessionKey [32]byte 38 | } 39 | 40 | // obfuscate adds multiplexing headers, encrypt and add TLS header 41 | func (o *Obfuscator) obfuscate(f *Frame, buf []byte, payloadOffsetInBuf int) (int, error) { 42 | // The method here is to use the first payloadCipher.NonceSize() bytes of the serialised frame header 43 | // as iv/nonce for the AEAD cipher to encrypt the frame payload. Then we use 44 | // the authentication tag produced appended to the end of the ciphertext (of size payloadCipher.Overhead()) 45 | // as nonce for Salsa20 to encrypt the frame header. Both with sessionKey as keys. 46 | // 47 | // Several cryptographic guarantees we have made here: that payloadCipher, as an AEAD, is given a unique 48 | // iv/nonce each time, relative to its key; that the frame header encryptor Salsa20 is given a unique 49 | // nonce each time, relative to its key; and that the authenticity of frame header is checked. 50 | // 51 | // The payloadCipher is given a unique iv/nonce each time because it is derived from the frame header, which 52 | // contains the monotonically increasing stream id (uint32) and frame sequence (uint64). There will be a nonce 53 | // reuse after 2^64-1 frames sent (sent, not received because frames going different ways are sequenced 54 | // independently) by a stream, or after 2^32-1 streams created in a single session. We consider these number 55 | // to be large enough that they may never happen in reasonable time frames. Of course, different sessions 56 | // will produce the same combination of stream id and frame sequence, but they will have different session keys. 57 | // 58 | // 59 | // Because the frame header, before it being encrypted, is fed into the AEAD, it is also authenticated. 60 | // (rfc5116 s.2.1 "The nonce is authenticated internally to the algorithm"). 61 | // 62 | // In case the user chooses to not encrypt the frame payload, payloadCipher will be nil. In this scenario, 63 | // we generate random bytes to be used as salsa20 nonce. 64 | payloadLen := len(f.Payload) 65 | if payloadLen == 0 { 66 | return 0, errors.New("payload cannot be empty") 67 | } 68 | tagLen := 0 69 | if o.payloadCipher != nil { 70 | tagLen = o.payloadCipher.Overhead() 71 | } else { 72 | tagLen = salsa20NonceSize 73 | } 74 | // Pad to avoid size side channel leak 75 | padLen := 0 76 | if f.Seq < padFirstNFrames { 77 | padLen = common.RandInt(maxExtraLen - tagLen + 1) 78 | } 79 | 80 | usefulLen := frameHeaderLength + payloadLen + padLen + tagLen 81 | if len(buf) < usefulLen { 82 | return 0, errors.New("obfs buffer too small") 83 | } 84 | // we do as much in-place as possible to save allocation 85 | payload := buf[frameHeaderLength : frameHeaderLength+payloadLen+padLen] 86 | if payloadOffsetInBuf != frameHeaderLength { 87 | // if payload is not at the correct location in buffer 88 | copy(payload, f.Payload) 89 | } 90 | 91 | header := buf[:frameHeaderLength] 92 | binary.BigEndian.PutUint32(header[0:4], f.StreamID) 93 | binary.BigEndian.PutUint64(header[4:12], f.Seq) 94 | header[12] = f.Closing 95 | header[13] = byte(padLen + tagLen) 96 | 97 | // Random bytes for padding and nonce 98 | _, err := rand.Read(buf[frameHeaderLength+payloadLen : usefulLen]) 99 | if err != nil { 100 | return 0, fmt.Errorf("failed to pad random: %w", err) 101 | } 102 | 103 | if o.payloadCipher != nil { 104 | o.payloadCipher.Seal(payload[:0], header[:o.payloadCipher.NonceSize()], payload, nil) 105 | } 106 | 107 | nonce := buf[usefulLen-salsa20NonceSize : usefulLen] 108 | salsa20.XORKeyStream(header, header, nonce, &o.sessionKey) 109 | 110 | return usefulLen, nil 111 | } 112 | 113 | // deobfuscate removes TLS header, decrypt and unmarshall frames 114 | func (o *Obfuscator) deobfuscate(f *Frame, in []byte) error { 115 | if len(in) < frameHeaderLength+salsa20NonceSize { 116 | return fmt.Errorf("input size %v, but it cannot be shorter than %v bytes", len(in), frameHeaderLength+salsa20NonceSize) 117 | } 118 | 119 | header := in[:frameHeaderLength] 120 | pldWithOverHead := in[frameHeaderLength:] // payload + potential overhead 121 | 122 | nonce := in[len(in)-salsa20NonceSize:] 123 | salsa20.XORKeyStream(header, header, nonce, &o.sessionKey) 124 | 125 | streamID := binary.BigEndian.Uint32(header[0:4]) 126 | seq := binary.BigEndian.Uint64(header[4:12]) 127 | closing := header[12] 128 | extraLen := header[13] 129 | 130 | usefulPayloadLen := len(pldWithOverHead) - int(extraLen) 131 | if usefulPayloadLen < 0 || usefulPayloadLen > len(pldWithOverHead) { 132 | return errors.New("extra length is negative or extra length is greater than total pldWithOverHead length") 133 | } 134 | 135 | var outputPayload []byte 136 | 137 | if o.payloadCipher == nil { 138 | if extraLen == 0 { 139 | outputPayload = pldWithOverHead 140 | } else { 141 | outputPayload = pldWithOverHead[:usefulPayloadLen] 142 | } 143 | } else { 144 | _, err := o.payloadCipher.Open(pldWithOverHead[:0], header[:o.payloadCipher.NonceSize()], pldWithOverHead, nil) 145 | if err != nil { 146 | return err 147 | } 148 | outputPayload = pldWithOverHead[:usefulPayloadLen] 149 | } 150 | 151 | f.StreamID = streamID 152 | f.Seq = seq 153 | f.Closing = closing 154 | f.Payload = outputPayload 155 | return nil 156 | } 157 | 158 | func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (o Obfuscator, err error) { 159 | o = Obfuscator{ 160 | sessionKey: sessionKey, 161 | } 162 | switch encryptionMethod { 163 | case EncryptionMethodPlain: 164 | o.payloadCipher = nil 165 | case EncryptionMethodAES256GCM: 166 | var c cipher.Block 167 | c, err = aes.NewCipher(sessionKey[:]) 168 | if err != nil { 169 | return 170 | } 171 | o.payloadCipher, err = cipher.NewGCM(c) 172 | if err != nil { 173 | return 174 | } 175 | case EncryptionMethodAES128GCM: 176 | var c cipher.Block 177 | c, err = aes.NewCipher(sessionKey[:16]) 178 | if err != nil { 179 | return 180 | } 181 | o.payloadCipher, err = cipher.NewGCM(c) 182 | if err != nil { 183 | return 184 | } 185 | case EncryptionMethodChaha20Poly1305: 186 | o.payloadCipher, err = chacha20poly1305.New(sessionKey[:]) 187 | if err != nil { 188 | return 189 | } 190 | default: 191 | return o, fmt.Errorf("unknown encryption method valued %v", encryptionMethod) 192 | } 193 | 194 | if o.payloadCipher != nil { 195 | if o.payloadCipher.NonceSize() > frameHeaderLength { 196 | return o, errors.New("payload AEAD's nonce size cannot be greater than size of frame header") 197 | } 198 | } 199 | 200 | return 201 | } 202 | -------------------------------------------------------------------------------- /internal/multiplex/obfs_test.go: -------------------------------------------------------------------------------- 1 | package multiplex 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "math/rand" 7 | "reflect" 8 | "testing" 9 | "testing/quick" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "golang.org/x/crypto/chacha20poly1305" 13 | ) 14 | 15 | func TestGenerateObfs(t *testing.T) { 16 | var sessionKey [32]byte 17 | rand.Read(sessionKey[:]) 18 | 19 | run := func(o Obfuscator, t *testing.T) { 20 | obfsBuf := make([]byte, 512) 21 | _testFrame, _ := quick.Value(reflect.TypeOf(Frame{}), rand.New(rand.NewSource(42))) 22 | testFrame := _testFrame.Interface().(Frame) 23 | i, err := o.obfuscate(&testFrame, obfsBuf, 0) 24 | assert.NoError(t, err) 25 | var resultFrame Frame 26 | 27 | err = o.deobfuscate(&resultFrame, obfsBuf[:i]) 28 | assert.NoError(t, err) 29 | assert.EqualValues(t, testFrame, resultFrame) 30 | } 31 | 32 | t.Run("plain", func(t *testing.T) { 33 | o, err := MakeObfuscator(EncryptionMethodPlain, sessionKey) 34 | assert.NoError(t, err) 35 | run(o, t) 36 | }) 37 | t.Run("aes-256-gcm", func(t *testing.T) { 38 | o, err := MakeObfuscator(EncryptionMethodAES256GCM, sessionKey) 39 | assert.NoError(t, err) 40 | run(o, t) 41 | }) 42 | t.Run("aes-128-gcm", func(t *testing.T) { 43 | o, err := MakeObfuscator(EncryptionMethodAES128GCM, sessionKey) 44 | assert.NoError(t, err) 45 | run(o, t) 46 | }) 47 | t.Run("chacha20-poly1305", func(t *testing.T) { 48 | o, err := MakeObfuscator(EncryptionMethodChaha20Poly1305, sessionKey) 49 | assert.NoError(t, err) 50 | run(o, t) 51 | }) 52 | t.Run("unknown encryption method", func(t *testing.T) { 53 | _, err := MakeObfuscator(0xff, sessionKey) 54 | assert.Error(t, err) 55 | }) 56 | } 57 | 58 | func TestObfuscate(t *testing.T) { 59 | var sessionKey [32]byte 60 | rand.Read(sessionKey[:]) 61 | 62 | const testPayloadLen = 1024 63 | testPayload := make([]byte, testPayloadLen) 64 | rand.Read(testPayload) 65 | f := Frame{ 66 | StreamID: 0, 67 | Seq: 0, 68 | Closing: 0, 69 | Payload: testPayload, 70 | } 71 | 72 | runTest := func(t *testing.T, o Obfuscator) { 73 | obfsBuf := make([]byte, testPayloadLen*2) 74 | n, err := o.obfuscate(&f, obfsBuf, 0) 75 | assert.NoError(t, err) 76 | 77 | resultFrame := Frame{} 78 | err = o.deobfuscate(&resultFrame, obfsBuf[:n]) 79 | assert.NoError(t, err) 80 | 81 | assert.EqualValues(t, f, resultFrame) 82 | } 83 | 84 | t.Run("plain", func(t *testing.T) { 85 | o := Obfuscator{ 86 | payloadCipher: nil, 87 | sessionKey: sessionKey, 88 | } 89 | runTest(t, o) 90 | }) 91 | 92 | t.Run("aes-128-gcm", func(t *testing.T) { 93 | c, err := aes.NewCipher(sessionKey[:16]) 94 | assert.NoError(t, err) 95 | payloadCipher, err := cipher.NewGCM(c) 96 | assert.NoError(t, err) 97 | o := Obfuscator{ 98 | payloadCipher: payloadCipher, 99 | sessionKey: sessionKey, 100 | } 101 | runTest(t, o) 102 | }) 103 | 104 | t.Run("aes-256-gcm", func(t *testing.T) { 105 | c, err := aes.NewCipher(sessionKey[:]) 106 | assert.NoError(t, err) 107 | payloadCipher, err := cipher.NewGCM(c) 108 | assert.NoError(t, err) 109 | o := Obfuscator{ 110 | payloadCipher: payloadCipher, 111 | sessionKey: sessionKey, 112 | } 113 | runTest(t, o) 114 | }) 115 | 116 | t.Run("chacha20-poly1305", func(t *testing.T) { 117 | payloadCipher, err := chacha20poly1305.New(sessionKey[:]) 118 | assert.NoError(t, err) 119 | o := Obfuscator{ 120 | payloadCipher: payloadCipher, 121 | sessionKey: sessionKey, 122 | } 123 | runTest(t, o) 124 | }) 125 | 126 | } 127 | 128 | func BenchmarkObfs(b *testing.B) { 129 | testPayload := make([]byte, 1024) 130 | rand.Read(testPayload) 131 | testFrame := &Frame{ 132 | 1, 133 | 0, 134 | 0, 135 | testPayload, 136 | } 137 | 138 | obfsBuf := make([]byte, len(testPayload)*2) 139 | 140 | var key [32]byte 141 | rand.Read(key[:]) 142 | b.Run("AES256GCM", func(b *testing.B) { 143 | c, _ := aes.NewCipher(key[:]) 144 | payloadCipher, _ := cipher.NewGCM(c) 145 | 146 | obfuscator := Obfuscator{ 147 | payloadCipher: payloadCipher, 148 | sessionKey: key, 149 | } 150 | 151 | b.SetBytes(int64(len(testFrame.Payload))) 152 | b.ResetTimer() 153 | for i := 0; i < b.N; i++ { 154 | obfuscator.obfuscate(testFrame, obfsBuf, 0) 155 | } 156 | }) 157 | b.Run("AES128GCM", func(b *testing.B) { 158 | c, _ := aes.NewCipher(key[:16]) 159 | payloadCipher, _ := cipher.NewGCM(c) 160 | 161 | obfuscator := Obfuscator{ 162 | payloadCipher: payloadCipher, 163 | sessionKey: key, 164 | } 165 | b.SetBytes(int64(len(testFrame.Payload))) 166 | b.ResetTimer() 167 | for i := 0; i < b.N; i++ { 168 | obfuscator.obfuscate(testFrame, obfsBuf, 0) 169 | } 170 | }) 171 | b.Run("plain", func(b *testing.B) { 172 | obfuscator := Obfuscator{ 173 | payloadCipher: nil, 174 | sessionKey: key, 175 | } 176 | b.SetBytes(int64(len(testFrame.Payload))) 177 | b.ResetTimer() 178 | for i := 0; i < b.N; i++ { 179 | obfuscator.obfuscate(testFrame, obfsBuf, 0) 180 | } 181 | }) 182 | b.Run("chacha20Poly1305", func(b *testing.B) { 183 | payloadCipher, _ := chacha20poly1305.New(key[:]) 184 | 185 | obfuscator := Obfuscator{ 186 | payloadCipher: payloadCipher, 187 | sessionKey: key, 188 | } 189 | b.SetBytes(int64(len(testFrame.Payload))) 190 | b.ResetTimer() 191 | for i := 0; i < b.N; i++ { 192 | obfuscator.obfuscate(testFrame, obfsBuf, 0) 193 | } 194 | }) 195 | } 196 | 197 | func BenchmarkDeobfs(b *testing.B) { 198 | testPayload := make([]byte, 1024) 199 | rand.Read(testPayload) 200 | testFrame := &Frame{ 201 | 1, 202 | 0, 203 | 0, 204 | testPayload, 205 | } 206 | 207 | obfsBuf := make([]byte, len(testPayload)*2) 208 | 209 | var key [32]byte 210 | rand.Read(key[:]) 211 | b.Run("AES256GCM", func(b *testing.B) { 212 | c, _ := aes.NewCipher(key[:]) 213 | payloadCipher, _ := cipher.NewGCM(c) 214 | obfuscator := Obfuscator{ 215 | payloadCipher: payloadCipher, 216 | sessionKey: key, 217 | } 218 | 219 | n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) 220 | 221 | frame := new(Frame) 222 | b.SetBytes(int64(n)) 223 | b.ResetTimer() 224 | for i := 0; i < b.N; i++ { 225 | obfuscator.deobfuscate(frame, obfsBuf[:n]) 226 | } 227 | }) 228 | b.Run("AES128GCM", func(b *testing.B) { 229 | c, _ := aes.NewCipher(key[:16]) 230 | payloadCipher, _ := cipher.NewGCM(c) 231 | 232 | obfuscator := Obfuscator{ 233 | payloadCipher: payloadCipher, 234 | sessionKey: key, 235 | } 236 | n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) 237 | 238 | frame := new(Frame) 239 | b.ResetTimer() 240 | b.SetBytes(int64(n)) 241 | for i := 0; i < b.N; i++ { 242 | obfuscator.deobfuscate(frame, obfsBuf[:n]) 243 | } 244 | }) 245 | b.Run("plain", func(b *testing.B) { 246 | obfuscator := Obfuscator{ 247 | payloadCipher: nil, 248 | sessionKey: key, 249 | } 250 | n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) 251 | 252 | frame := new(Frame) 253 | b.ResetTimer() 254 | b.SetBytes(int64(n)) 255 | for i := 0; i < b.N; i++ { 256 | obfuscator.deobfuscate(frame, obfsBuf[:n]) 257 | } 258 | }) 259 | b.Run("chacha20Poly1305", func(b *testing.B) { 260 | payloadCipher, _ := chacha20poly1305.New(key[:]) 261 | 262 | obfuscator := Obfuscator{ 263 | payloadCipher: payloadCipher, 264 | sessionKey: key, 265 | } 266 | 267 | n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) 268 | 269 | frame := new(Frame) 270 | b.ResetTimer() 271 | b.SetBytes(int64(n)) 272 | for i := 0; i < b.N; i++ { 273 | obfuscator.deobfuscate(frame, obfsBuf[:n]) 274 | } 275 | }) 276 | } 277 | --------------------------------------------------------------------------------