Merge branch 'master' into notsure2

This commit is contained in:
notsure2 2024-10-12 01:39:42 +03:00
commit 02d9a4655a
7 changed files with 145 additions and 58 deletions

View File

@ -7,11 +7,85 @@ jobs:
matrix: matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ] os: [ ubuntu-latest, macos-latest, windows-latest ]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-go@v2 - uses: actions/setup-go@v5
with: with:
go-version: '^1.22' # The Go version to download (if necessary) and use. go-version: '^1.22' # The Go version to download (if necessary) and use.
- run: go test -race -coverprofile coverage.txt -coverpkg ./... -covermode atomic ./... - run: go test -race -coverprofile coverage.txt -coverpkg ./... -covermode atomic ./...
- uses: codecov/codecov-action@v1 - uses: codecov/codecov-action@v4
with: with:
file: coverage.txt files: coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
compat-test:
runs-on: ubuntu-latest
strategy:
matrix:
encryption-method: [ plain, chacha20-poly1305 ]
num-conn: [ 0, 1, 4 ]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '^1.22'
- name: Build Cloak
run: make
- name: Create configs
run: |
mkdir config
cat << EOF > config/ckclient.json
{
"Transport": "direct",
"ProxyMethod": "iperf",
"EncryptionMethod": "${{ matrix.encryption-method }}",
"UID": "Q4GAXHVgnDLXsdTpw6bmoQ==",
"PublicKey": "4dae/bF43FKGq+QbCc5P/E/MPM5qQeGIArjmJEHiZxc=",
"ServerName": "cloudflare.com",
"BrowserSig": "firefox",
"NumConn": ${{ matrix.num-conn }}
}
EOF
cat << EOF > config/ckserver.json
{
"ProxyBook": {
"iperf": [
"tcp",
"127.0.0.1:5201"
]
},
"BindAddr": [
":8443"
],
"BypassUID": [
"Q4GAXHVgnDLXsdTpw6bmoQ=="
],
"RedirAddr": "cloudflare.com",
"PrivateKey": "AAaskZJRPIAbiuaRLHsvZPvE6gzOeSjg+ZRg1ENau0Y="
}
EOF
- name: Start iperf3 server
run: docker run -d --name iperf-server --network host ajoergensen/iperf3:latest --server
- name: Test new client against old server
run: |
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
build/ck-client -c config/ckclient.json -s 127.0.0.1 -p 8443 --verbosity debug | tee new-cloak-client.log &
docker run --network host ajoergensen/iperf3:latest --client 127.0.0.1 -p 1984
docker stop old-cloak-server
- name: Test old client against new server
run: |
build/ck-server -c config/ckserver.json --verbosity debug | tee new-cloak-server.log &
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
docker run --network host ajoergensen/iperf3:latest --client 127.0.0.1 -p 1984
docker stop old-cloak-client
- name: Dump docker logs
if: always()
run: |
docker container logs iperf-server > iperf-server.log
docker container logs old-cloak-server > old-cloak-server.log
docker container logs old-cloak-client > old-cloak-client.log
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.encryption-method }}-${{ matrix.num-conn }}-conn-logs
path: ./*.log

View File

@ -9,7 +9,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Build - name: Build
run: | run: |
export PATH=${PATH}:`go env GOPATH`/bin export PATH=${PATH}:`go env GOPATH`/bin
@ -20,3 +20,31 @@ jobs:
files: release/* files: release/*
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-docker:
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
cbeuw/cloak
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM golang:latest
RUN git clone https://github.com/cbeuw/Cloak.git
WORKDIR Cloak
RUN make

View File

@ -3,10 +3,10 @@ package multiplex
import ( import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common" "github.com/cbeuw/Cloak/internal/common"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/salsa20" "golang.org/x/crypto/salsa20"
@ -15,6 +15,14 @@ import (
const frameHeaderLength = 14 const frameHeaderLength = 14
const salsa20NonceSize = 8 const salsa20NonceSize = 8
// maxExtraLen equals the max length of padding + AEAD tag.
// It is 255 bytes because the extra len field in frame header is only one byte.
const maxExtraLen = 1<<8 - 1
// padFirstNFrames specifies the number of initial frames to pad,
// to avoid TLS-in-TLS detection
const padFirstNFrames = 5
const ( const (
EncryptionMethodPlain = iota EncryptionMethodPlain = iota
EncryptionMethodAES256GCM EncryptionMethodAES256GCM
@ -27,8 +35,6 @@ type Obfuscator struct {
payloadCipher cipher.AEAD payloadCipher cipher.AEAD
sessionKey [32]byte sessionKey [32]byte
maxOverhead int
} }
// obfuscate adds multiplexing headers, encrypt and add TLS header // obfuscate adds multiplexing headers, encrypt and add TLS header
@ -49,45 +55,34 @@ func (o *Obfuscator) obfuscate(f *Frame, buf []byte, payloadOffsetInBuf int) (in
// to be large enough that they may never happen in reasonable time frames. Of course, different sessions // to be large enough that they may never happen in reasonable time frames. Of course, different sessions
// will produce the same combination of stream id and frame sequence, but they will have different session keys. // will produce the same combination of stream id and frame sequence, but they will have different session keys.
// //
// Salsa20 is assumed to be given a unique nonce each time because we assume the tags produced by payloadCipher
// AEAD is unique each time, as payloadCipher itself is given a unique iv/nonce each time due to points made above.
// This is relatively a weak guarantee as we are assuming AEADs to produce different tags given different iv/nonces.
// This is almost certainly true but I cannot find a source that outright states this.
// //
// Because the frame header, before it being encrypted, is fed into the AEAD, it is also authenticated. // Because the frame header, before it being encrypted, is fed into the AEAD, it is also authenticated.
// (rfc5116 s.2.1 "The nonce is authenticated internally to the algorithm"). // (rfc5116 s.2.1 "The nonce is authenticated internally to the algorithm").
// //
// In case the user chooses to not encrypt the frame payload, payloadCipher will be nil. In this scenario, // In case the user chooses to not encrypt the frame payload, payloadCipher will be nil. In this scenario,
// we pad the frame payload with random bytes until it reaches Salsa20's nonce size (8 bytes). Then we simply // we generate random bytes to be used as salsa20 nonce.
// encrypt the frame header with the last 8 bytes of frame payload as nonce.
// If the payload provided by the user is greater than 8 bytes, then we use entirely the user input as nonce.
// We can't ensure its uniqueness ourselves, which is why plaintext mode must only be used when the user input
// is already random-like. For Cloak it would normally mean that the user is using a proxy protocol that sends
// encrypted data.
payloadLen := len(f.Payload) payloadLen := len(f.Payload)
if payloadLen == 0 { if payloadLen == 0 {
return 0, errors.New("payload cannot be empty") return 0, errors.New("payload cannot be empty")
} }
var extraLen int tagLen := 0
if o.payloadCipher == nil { if o.payloadCipher != nil {
extraLen = salsa20NonceSize - payloadLen tagLen = o.payloadCipher.Overhead()
if extraLen < 0 {
// if our payload is already greater than 8 bytes
extraLen = 0
}
} else { } else {
extraLen = o.payloadCipher.Overhead() tagLen = salsa20NonceSize
if extraLen < salsa20NonceSize { }
return 0, errors.New("AEAD's Overhead cannot be fewer than 8 bytes") // Pad to avoid size side channel leak
} padLen := 0
if f.Seq < padFirstNFrames {
padLen = common.RandInt(maxExtraLen - tagLen + 1)
} }
usefulLen := frameHeaderLength + payloadLen + extraLen usefulLen := frameHeaderLength + payloadLen + padLen + tagLen
if len(buf) < usefulLen { if len(buf) < usefulLen {
return 0, errors.New("obfs buffer too small") return 0, errors.New("obfs buffer too small")
} }
// we do as much in-place as possible to save allocation // we do as much in-place as possible to save allocation
payload := buf[frameHeaderLength : frameHeaderLength+payloadLen] payload := buf[frameHeaderLength : frameHeaderLength+payloadLen+padLen]
if payloadOffsetInBuf != frameHeaderLength { if payloadOffsetInBuf != frameHeaderLength {
// if payload is not at the correct location in buffer // if payload is not at the correct location in buffer
copy(payload, f.Payload) copy(payload, f.Payload)
@ -97,14 +92,15 @@ func (o *Obfuscator) obfuscate(f *Frame, buf []byte, payloadOffsetInBuf int) (in
binary.BigEndian.PutUint32(header[0:4], f.StreamID) binary.BigEndian.PutUint32(header[0:4], f.StreamID)
binary.BigEndian.PutUint64(header[4:12], f.Seq) binary.BigEndian.PutUint64(header[4:12], f.Seq)
header[12] = f.Closing header[12] = f.Closing
header[13] = byte(extraLen) header[13] = byte(padLen + tagLen)
if o.payloadCipher == nil { // Random bytes for padding and nonce
if extraLen != 0 { // read nonce _, err := rand.Read(buf[frameHeaderLength+payloadLen : usefulLen])
extra := buf[usefulLen-extraLen : usefulLen] if err != nil {
common.CryptoRandRead(extra) return 0, fmt.Errorf("failed to pad random: %w", err)
} }
} else {
if o.payloadCipher != nil {
o.payloadCipher.Seal(payload[:0], header[:o.payloadCipher.NonceSize()], payload, nil) o.payloadCipher.Seal(payload[:0], header[:o.payloadCipher.NonceSize()], payload, nil)
} }
@ -166,7 +162,6 @@ func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (o Obfuscator, e
switch encryptionMethod { switch encryptionMethod {
case EncryptionMethodPlain: case EncryptionMethodPlain:
o.payloadCipher = nil o.payloadCipher = nil
o.maxOverhead = salsa20NonceSize
case EncryptionMethodAES256GCM: case EncryptionMethodAES256GCM:
var c cipher.Block var c cipher.Block
c, err = aes.NewCipher(sessionKey[:]) c, err = aes.NewCipher(sessionKey[:])
@ -177,7 +172,6 @@ func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (o Obfuscator, e
if err != nil { if err != nil {
return return
} }
o.maxOverhead = o.payloadCipher.Overhead()
case EncryptionMethodAES128GCM: case EncryptionMethodAES128GCM:
var c cipher.Block var c cipher.Block
c, err = aes.NewCipher(sessionKey[:16]) c, err = aes.NewCipher(sessionKey[:16])
@ -188,13 +182,11 @@ func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (o Obfuscator, e
if err != nil { if err != nil {
return return
} }
o.maxOverhead = o.payloadCipher.Overhead()
case EncryptionMethodChaha20Poly1305: case EncryptionMethodChaha20Poly1305:
o.payloadCipher, err = chacha20poly1305.New(sessionKey[:]) o.payloadCipher, err = chacha20poly1305.New(sessionKey[:])
if err != nil { if err != nil {
return return
} }
o.maxOverhead = o.payloadCipher.Overhead()
default: default:
return o, fmt.Errorf("unknown encryption method valued %v", encryptionMethod) return o, fmt.Errorf("unknown encryption method valued %v", encryptionMethod)
} }

View File

@ -85,7 +85,6 @@ func TestObfuscate(t *testing.T) {
o := Obfuscator{ o := Obfuscator{
payloadCipher: nil, payloadCipher: nil,
sessionKey: sessionKey, sessionKey: sessionKey,
maxOverhead: salsa20NonceSize,
} }
runTest(t, o) runTest(t, o)
}) })
@ -98,7 +97,6 @@ func TestObfuscate(t *testing.T) {
o := Obfuscator{ o := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: sessionKey, sessionKey: sessionKey,
maxOverhead: payloadCipher.Overhead(),
} }
runTest(t, o) runTest(t, o)
}) })
@ -111,7 +109,6 @@ func TestObfuscate(t *testing.T) {
o := Obfuscator{ o := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: sessionKey, sessionKey: sessionKey,
maxOverhead: payloadCipher.Overhead(),
} }
runTest(t, o) runTest(t, o)
}) })
@ -122,7 +119,6 @@ func TestObfuscate(t *testing.T) {
o := Obfuscator{ o := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: sessionKey, sessionKey: sessionKey,
maxOverhead: payloadCipher.Overhead(),
} }
runTest(t, o) runTest(t, o)
}) })
@ -150,7 +146,6 @@ func BenchmarkObfs(b *testing.B) {
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: key, sessionKey: key,
maxOverhead: payloadCipher.Overhead(),
} }
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
@ -166,7 +161,6 @@ func BenchmarkObfs(b *testing.B) {
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: key, sessionKey: key,
maxOverhead: payloadCipher.Overhead(),
} }
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
b.ResetTimer() b.ResetTimer()
@ -178,7 +172,6 @@ func BenchmarkObfs(b *testing.B) {
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: nil, payloadCipher: nil,
sessionKey: key, sessionKey: key,
maxOverhead: salsa20NonceSize,
} }
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
b.ResetTimer() b.ResetTimer()
@ -192,7 +185,6 @@ func BenchmarkObfs(b *testing.B) {
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: key, sessionKey: key,
maxOverhead: payloadCipher.Overhead(),
} }
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
b.ResetTimer() b.ResetTimer()
@ -222,7 +214,6 @@ func BenchmarkDeobfs(b *testing.B) {
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: key, sessionKey: key,
maxOverhead: payloadCipher.Overhead(),
} }
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
@ -241,7 +232,6 @@ func BenchmarkDeobfs(b *testing.B) {
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: payloadCipher, payloadCipher: payloadCipher,
sessionKey: key, sessionKey: key,
maxOverhead: payloadCipher.Overhead(),
} }
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
@ -256,7 +246,6 @@ func BenchmarkDeobfs(b *testing.B) {
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: nil, payloadCipher: nil,
sessionKey: key, sessionKey: key,
maxOverhead: salsa20NonceSize,
} }
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
@ -271,9 +260,8 @@ func BenchmarkDeobfs(b *testing.B) {
payloadCipher, _ := chacha20poly1305.New(key[:]) payloadCipher, _ := chacha20poly1305.New(key[:])
obfuscator := Obfuscator{ obfuscator := Obfuscator{
payloadCipher: nil, payloadCipher: payloadCipher,
sessionKey: key, sessionKey: key,
maxOverhead: payloadCipher.Overhead(),
} }
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0) n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)

View File

@ -108,7 +108,7 @@ func MakeSession(id uint32, config SessionConfig) *Session {
sesh.InactivityTimeout = defaultInactivityTimeout sesh.InactivityTimeout = defaultInactivityTimeout
} }
sesh.maxStreamUnitWrite = sesh.MsgOnWireSizeLimit - frameHeaderLength - sesh.maxOverhead sesh.maxStreamUnitWrite = sesh.MsgOnWireSizeLimit - frameHeaderLength - maxExtraLen
sesh.streamSendBufferSize = sesh.MsgOnWireSizeLimit sesh.streamSendBufferSize = sesh.MsgOnWireSizeLimit
sesh.connReceiveBufferSize = 20480 // for backwards compatibility sesh.connReceiveBufferSize = 20480 // for backwards compatibility

View File

@ -61,7 +61,7 @@ func decryptClientInfo(fragments authFragments, serverTime time.Time) (info Clie
var ErrReplay = errors.New("duplicate random") var ErrReplay = errors.New("duplicate random")
var ErrBadProxyMethod = errors.New("invalid proxy method") var ErrBadProxyMethod = errors.New("invalid proxy method")
var ErrBadDecryption = errors.New("decryption/authentication faliure") var ErrBadDecryption = errors.New("decryption/authentication failure")
// AuthFirstPacket checks if the first packet of data is ClientHello or HTTP GET, and checks if it was from a Cloak client // AuthFirstPacket checks if the first packet of data is ClientHello or HTTP GET, and checks if it was from a Cloak client
// if it is from a Cloak client, it returns the ClientInfo with the decrypted fields. It doesn't check if the user // if it is from a Cloak client, it returns the ClientInfo with the decrypted fields. It doesn't check if the user