mirror of https://github.com/cbeuw/Cloak
Compare commits
42 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c3d5470ef7 | |
|
|
8f629c7b2e | |
|
|
49357fc20b | |
|
|
8af137637e | |
|
|
51ed286f35 | |
|
|
5146ea8503 | |
|
|
7f9c17439f | |
|
|
c15fd730de | |
|
|
d06c208ace | |
|
|
9800e3685d | |
|
|
07aa197061 | |
|
|
cfdd5e6560 | |
|
|
64166bf580 | |
|
|
d229d8b3dc | |
|
|
8bbc7b08d3 | |
|
|
5cf975f596 | |
|
|
19c8cd1f89 | |
|
|
5867fa932b | |
|
|
bfaf46d2e9 | |
|
|
e362e81d19 | |
|
|
deb0d26c08 | |
|
|
3687087c67 | |
|
|
97a03139bc | |
|
|
b3c6426ac5 | |
|
|
dc2e83f75f | |
|
|
5988b4337d | |
|
|
de4dab6bf3 | |
|
|
d5da5d049c | |
|
|
392fc41de8 | |
|
|
3b449b64b3 | |
|
|
a848d2f7e5 | |
|
|
de1c7600c1 | |
|
|
767716b9be | |
|
|
1cc4a1f928 | |
|
|
82687d4419 | |
|
|
6b08af0c18 | |
|
|
c48a8800d6 | |
|
|
c5b31de753 | |
|
|
b9907c2e18 | |
|
|
6417e3393d | |
|
|
b3ec1ab3bc | |
|
|
eca5f13936 |
|
|
@ -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.17' # The Go version to download (if necessary) and use.
|
go-version: '^1.24' # 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.24'
|
||||||
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
FROM golang:latest
|
||||||
|
|
||||||
|
RUN git clone https://github.com/cbeuw/Cloak.git
|
||||||
|
WORKDIR Cloak
|
||||||
|
RUN make
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<img src="https://user-images.githubusercontent.com/7034308/155629720-54dd8758-ec98-4fed-b603-623f0ad83b6c.svg" />
|
<img src="https://user-images.githubusercontent.com/7034308/155629720-54dd8758-ec98-4fed-b603-623f0ad83b6c.svg" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Cloak is a [pluggable transport](https://www.ietf.org/proceedings/103/slides/slides-103-pearg-pt-slides-01) that enhances
|
Cloak is a [pluggable transport](https://datatracker.ietf.org/meeting/103/materials/slides-103-pearg-pt-slides-01) that enhances
|
||||||
traditional proxy tools like OpenVPN to evade [sophisticated censorship](https://en.wikipedia.org/wiki/Deep_packet_inspection) and [data discrimination](https://en.wikipedia.org/wiki/Net_bias).
|
traditional proxy tools like OpenVPN to evade [sophisticated censorship](https://en.wikipedia.org/wiki/Deep_packet_inspection) and [data discrimination](https://en.wikipedia.org/wiki/Net_bias).
|
||||||
|
|
||||||
Cloak is not a standalone proxy program. Rather, it works by masquerading proxied traffic as normal web browsing
|
Cloak is not a standalone proxy program. Rather, it works by masquerading proxied traffic as normal web browsing
|
||||||
|
|
@ -137,7 +137,7 @@ random-like. **You may only leave it as `plain` if you are certain that your und
|
||||||
encryption and authentication (via AEAD or similar techniques).**
|
encryption and authentication (via AEAD or similar techniques).**
|
||||||
|
|
||||||
`ServerName` is the domain you want to make your ISP or firewall _think_ you are visiting. Ideally it should
|
`ServerName` is the domain you want to make your ISP or firewall _think_ you are visiting. Ideally it should
|
||||||
match `RedirAddr` in the server's configuration, a major site the censor allows, but it doesn't have to.
|
match `RedirAddr` in the server's configuration, a major site the censor allows, but it doesn't have to. Use `random` to randomize the server name for every connection made.
|
||||||
|
|
||||||
`AlternativeNames` is an array used alongside `ServerName` to shuffle between different ServerNames for every new
|
`AlternativeNames` is an array used alongside `ServerName` to shuffle between different ServerNames for every new
|
||||||
connection. **This may conflict with `CDN` Transport mode** if the CDN provider prohibits domain fronting and rejects
|
connection. **This may conflict with `CDN` Transport mode** if the CDN provider prohibits domain fronting and rejects
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
coverage:
|
coverage:
|
||||||
status:
|
status:
|
||||||
project:
|
project: off
|
||||||
default:
|
patch: off
|
||||||
threshold: 1%
|
|
||||||
35
go.mod
35
go.mod
|
|
@ -1,17 +1,30 @@
|
||||||
module github.com/cbeuw/Cloak
|
module github.com/cbeuw/Cloak
|
||||||
|
|
||||||
go 1.14
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
|
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/juju/ratelimit v1.0.1
|
github.com/juju/ratelimit v1.0.2
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/refraction-networking/utls v1.8.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.10.0
|
||||||
gitlab.com/yawning/utls.git v0.0.12-1
|
go.etcd.io/bbolt v1.4.0
|
||||||
go.etcd.io/bbolt v1.3.6
|
golang.org/x/crypto v0.37.0
|
||||||
golang.org/x/crypto v0.1.0
|
)
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
113
go.sum
113
go.sum
|
|
@ -1,76 +1,61 @@
|
||||||
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs=
|
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs=
|
||||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE=
|
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE=
|
||||||
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||||
|
github.com/refraction-networking/utls v1.7.0/go.mod h1:lV0Gwc1/Fi+HYH8hOtgFRdHfKo4FKSn6+FdyOz9hRms=
|
||||||
|
github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
|
||||||
|
github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
|
||||||
|
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
||||||
|
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
|
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||||
gitlab.com/yawning/utls.git v0.0.12-1 h1:RL6O0MP2YI0KghuEU/uGN6+8b4183eqNWoYgx7CXD0U=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
gitlab.com/yawning/utls.git v0.0.12-1/go.mod h1:3ONKiSFR9Im/c3t5RKmMJTVdmZN496FNyk3mjrY1dyo=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/cbeuw/Cloak/internal/common"
|
"github.com/cbeuw/Cloak/internal/common"
|
||||||
|
utls "github.com/refraction-networking/utls"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appDataMaxLength = 16401
|
const appDataMaxLength = 16401
|
||||||
|
|
@ -18,63 +17,122 @@ type clientHelloFields struct {
|
||||||
serverName string
|
serverName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeHex(s string) []byte {
|
type browser int
|
||||||
b, err := hex.DecodeString(s)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
type browser interface {
|
const (
|
||||||
composeClientHello(clientHelloFields) []byte
|
chrome = iota
|
||||||
}
|
firefox
|
||||||
|
safari
|
||||||
func generateSNI(serverName string) []byte {
|
)
|
||||||
serverNameListLength := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(serverNameListLength, uint16(len(serverName)+3))
|
|
||||||
serverNameType := []byte{0x00} // host_name
|
|
||||||
serverNameLength := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(serverNameLength, uint16(len(serverName)))
|
|
||||||
ret := make([]byte, 2+1+2+len(serverName))
|
|
||||||
copy(ret[0:2], serverNameListLength)
|
|
||||||
copy(ret[2:3], serverNameType)
|
|
||||||
copy(ret[3:5], serverNameLength)
|
|
||||||
copy(ret[5:], serverName)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// addExtensionRecord, add type, length to extension data
|
|
||||||
func addExtRec(typ []byte, data []byte) []byte {
|
|
||||||
length := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(length, uint16(len(data)))
|
|
||||||
ret := make([]byte, 2+2+len(data))
|
|
||||||
copy(ret[0:2], typ)
|
|
||||||
copy(ret[2:4], length)
|
|
||||||
copy(ret[4:], data)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
type DirectTLS struct {
|
type DirectTLS struct {
|
||||||
*common.TLSConn
|
*common.TLSConn
|
||||||
browser browser
|
browser browser
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientTransport handles the TLS handshake for a given conn and returns the sessionKey
|
var topLevelDomains = []string{"com", "net", "org", "it", "fr", "me", "ru", "cn", "es", "tr", "top", "xyz", "info"}
|
||||||
|
|
||||||
|
func randomServerName() string {
|
||||||
|
/*
|
||||||
|
Copyright: Proton AG
|
||||||
|
https://github.com/ProtonVPN/wireguard-go/commit/bcf344b39b213c1f32147851af0d2a8da9266883
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
charNum := int('z') - int('a') + 1
|
||||||
|
size := 3 + common.RandInt(10)
|
||||||
|
name := make([]byte, size)
|
||||||
|
for i := range name {
|
||||||
|
name[i] = byte(int('a') + common.RandInt(charNum))
|
||||||
|
}
|
||||||
|
return string(name) + "." + common.RandItem(topLevelDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildClientHello(browser browser, fields clientHelloFields) ([]byte, error) {
|
||||||
|
// We don't use utls to handle connections (as it'll attempt a real TLS negotiation)
|
||||||
|
// We only want it to build the ClientHello locally
|
||||||
|
fakeConn := net.TCPConn{}
|
||||||
|
var helloID utls.ClientHelloID
|
||||||
|
switch browser {
|
||||||
|
case chrome:
|
||||||
|
helloID = utls.HelloChrome_Auto
|
||||||
|
case firefox:
|
||||||
|
helloID = utls.HelloFirefox_Auto
|
||||||
|
case safari:
|
||||||
|
helloID = utls.HelloSafari_Auto
|
||||||
|
}
|
||||||
|
|
||||||
|
uclient := utls.UClient(&fakeConn, &utls.Config{ServerName: fields.serverName}, helloID)
|
||||||
|
if err := uclient.BuildHandshakeState(); err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
if err := uclient.SetClientRandom(fields.random); err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uclient.HandshakeState.Hello.SessionId = make([]byte, 32)
|
||||||
|
copy(uclient.HandshakeState.Hello.SessionId, fields.sessionId)
|
||||||
|
|
||||||
|
// Find the X25519 key share and overwrite it
|
||||||
|
var extIndex int
|
||||||
|
var keyShareIndex int
|
||||||
|
for i, ext := range uclient.Extensions {
|
||||||
|
ext, ok := ext.(*utls.KeyShareExtension)
|
||||||
|
if ok {
|
||||||
|
extIndex = i
|
||||||
|
for j, keyShare := range ext.KeyShares {
|
||||||
|
if keyShare.Group == utls.X25519 {
|
||||||
|
keyShareIndex = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy(uclient.Extensions[extIndex].(*utls.KeyShareExtension).KeyShares[keyShareIndex].Data, fields.x25519KeyShare)
|
||||||
|
|
||||||
|
if err := uclient.BuildHandshakeState(); err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
return uclient.HandshakeState.Hello.Raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handshake handles the TLS handshake for a given conn and returns the sessionKey
|
||||||
// if the server proceed with Cloak authentication
|
// if the server proceed with Cloak authentication
|
||||||
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
|
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
|
||||||
payload, sharedSecret := makeAuthenticationPayload(authInfo)
|
payload, sharedSecret := makeAuthenticationPayload(authInfo)
|
||||||
|
|
||||||
// random is marshalled ephemeral pub key 32 bytes
|
|
||||||
// The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare
|
|
||||||
fields := clientHelloFields{
|
fields := clientHelloFields{
|
||||||
random: payload.randPubKey[:],
|
random: payload.randPubKey[:],
|
||||||
sessionId: payload.ciphertextWithTag[0:32],
|
sessionId: payload.ciphertextWithTag[0:32],
|
||||||
x25519KeyShare: payload.ciphertextWithTag[32:64],
|
x25519KeyShare: payload.ciphertextWithTag[32:64],
|
||||||
serverName: authInfo.MockDomain,
|
serverName: authInfo.MockDomain,
|
||||||
}
|
}
|
||||||
chOnly := tls.browser.composeClientHello(fields)
|
|
||||||
chWithRecordLayer := common.AddRecordLayer(chOnly, common.Handshake, common.VersionTLS11)
|
if strings.EqualFold(fields.serverName, "random") {
|
||||||
|
fields.serverName = randomServerName()
|
||||||
|
}
|
||||||
|
|
||||||
|
var ch []byte
|
||||||
|
ch, err = buildClientHello(tls.browser, fields)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chWithRecordLayer := common.AddRecordLayer(ch, common.Handshake, common.VersionTLS11)
|
||||||
_, err = rawConn.Write(chWithRecordLayer)
|
_, err = rawConn.Write(chWithRecordLayer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func htob(s string) []byte {
|
|
||||||
b, _ := hex.DecodeString(s)
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeServerName(t *testing.T) {
|
|
||||||
type testingPair struct {
|
|
||||||
serverName string
|
|
||||||
target []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
pairs := []testingPair{
|
|
||||||
{
|
|
||||||
"www.google.com",
|
|
||||||
htob("001100000e7777772e676f6f676c652e636f6d"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"www.gstatic.com",
|
|
||||||
htob("001200000f7777772e677374617469632e636f6d"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"googleads.g.doubleclick.net",
|
|
||||||
htob("001e00001b676f6f676c656164732e672e646f75626c65636c69636b2e6e6574"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range pairs {
|
|
||||||
assert.Equal(t, p.target, generateSNI(p.serverName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
// Fingerprint of Chrome 112
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"github.com/cbeuw/Cloak/internal/common"
|
|
||||||
"math/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Chrome struct{}
|
|
||||||
|
|
||||||
func makeGREASE() []byte {
|
|
||||||
// see https://tools.ietf.org/html/draft-davidben-tls-grease-01
|
|
||||||
// This is exclusive to Chrome.
|
|
||||||
var one [1]byte
|
|
||||||
common.CryptoRandRead(one[:])
|
|
||||||
sixteenth := one[0] % 16
|
|
||||||
monoGREASE := sixteenth*16 + 0xA
|
|
||||||
doubleGREASE := []byte{monoGREASE, monoGREASE}
|
|
||||||
return doubleGREASE
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) composeExtensions(serverName string, keyShare []byte) []byte {
|
|
||||||
|
|
||||||
makeSupportedGroups := func() []byte {
|
|
||||||
suppGroupListLen := []byte{0x00, 0x08}
|
|
||||||
ret := make([]byte, 2+8)
|
|
||||||
copy(ret[0:2], suppGroupListLen)
|
|
||||||
copy(ret[2:4], makeGREASE())
|
|
||||||
copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18})
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
makeKeyShare := func(hidden []byte) []byte {
|
|
||||||
ret := make([]byte, 43)
|
|
||||||
ret[0], ret[1] = 0x00, 0x29 // length 41
|
|
||||||
copy(ret[2:4], makeGREASE())
|
|
||||||
ret[4], ret[5] = 0x00, 0x01 // length 1
|
|
||||||
ret[6] = 0x00
|
|
||||||
ret[7], ret[8] = 0x00, 0x1d // group x25519
|
|
||||||
ret[9], ret[10] = 0x00, 0x20 // length 32
|
|
||||||
copy(ret[11:43], hidden)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffle := func(exts [][]byte) {
|
|
||||||
var qword [8]byte
|
|
||||||
common.CryptoRandRead(qword[:])
|
|
||||||
seed := int64(binary.BigEndian.Uint64(qword[:]))
|
|
||||||
source := rand.NewSource(seed)
|
|
||||||
r := rand.New(source)
|
|
||||||
r.Shuffle(len(exts), func(i, j int) { exts[i], exts[j] = exts[j], exts[i] })
|
|
||||||
}
|
|
||||||
|
|
||||||
// extension length is always 403, and server name length is variable
|
|
||||||
var ext [18][]byte
|
|
||||||
ext[0] = addExtRec(makeGREASE(), nil) // First GREASE
|
|
||||||
|
|
||||||
// Start shufflable extensions: https://chromestatus.com/feature/5124606246518784
|
|
||||||
ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
|
|
||||||
sniLen := len(ext[1])
|
|
||||||
ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
|
|
||||||
ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
|
|
||||||
ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups
|
|
||||||
ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
|
|
||||||
ext[6] = addExtRec([]byte{0x00, 0x23}, nil) // Session tickets
|
|
||||||
ext[7] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
|
|
||||||
ext[8] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
|
|
||||||
ext[9] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001004030804040105030805050108060601")) // Signature Algorithms
|
|
||||||
ext[10] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp
|
|
||||||
ext[11] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share
|
|
||||||
ext[12] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
|
|
||||||
suppVersions := decodeHex("069A9A03040303") // 9A9A needs to be a GREASE
|
|
||||||
copy(suppVersions[1:3], makeGREASE())
|
|
||||||
ext[13] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
|
|
||||||
ext[14] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x02}) // compress certificate
|
|
||||||
ext[15] = addExtRec([]byte{0x44, 0x69}, decodeHex("0003026832")) // application settings
|
|
||||||
// End shufflable extensions
|
|
||||||
|
|
||||||
shuffle(ext[1:16])
|
|
||||||
|
|
||||||
ext[16] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE
|
|
||||||
// sniLen + len(all other ext) + len(ext[17]) = 403
|
|
||||||
// len(all other ext) = 175
|
|
||||||
// len(ext[17]) = 228 - sniLen
|
|
||||||
// 2+2+len(padding) = 228 - sniLen
|
|
||||||
// len(padding) = 224 - sniLen
|
|
||||||
ext[17] = addExtRec([]byte{0x00, 0x15}, make([]byte, 224-sniLen)) // padding
|
|
||||||
var ret []byte
|
|
||||||
for _, e := range ext {
|
|
||||||
ret = append(ret, e...)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Chrome) composeClientHello(hd clientHelloFields) (ch []byte) {
|
|
||||||
var clientHello [12][]byte
|
|
||||||
clientHello[0] = []byte{0x01} // handshake type
|
|
||||||
clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508
|
|
||||||
clientHello[2] = []byte{0x03, 0x03} // client version
|
|
||||||
clientHello[3] = hd.random // random
|
|
||||||
clientHello[4] = []byte{0x20} // session id length 32
|
|
||||||
clientHello[5] = hd.sessionId // session id
|
|
||||||
clientHello[6] = []byte{0x00, 0x20} // cipher suites length 32
|
|
||||||
clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035")...) // cipher suites
|
|
||||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
|
||||||
clientHello[9] = []byte{0x00} // compression methods
|
|
||||||
|
|
||||||
extensions := c.composeExtensions(hd.serverName, hd.x25519KeyShare)
|
|
||||||
clientHello[10] = []byte{0x00, 0x00}
|
|
||||||
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
|
|
||||||
clientHello[11] = extensions
|
|
||||||
|
|
||||||
var ret []byte
|
|
||||||
for _, c := range clientHello {
|
|
||||||
ret = append(ret, c...)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMakeGREASE(t *testing.T) {
|
|
||||||
a := hex.EncodeToString(makeGREASE())
|
|
||||||
if a[1] != 'a' || a[3] != 'a' {
|
|
||||||
t.Errorf("GREASE got %v", a)
|
|
||||||
}
|
|
||||||
|
|
||||||
var GREASEs []string
|
|
||||||
for i := 0; i < 50; i++ {
|
|
||||||
GREASEs = append(GREASEs, hex.EncodeToString(makeGREASE()))
|
|
||||||
}
|
|
||||||
var eqCount int
|
|
||||||
for _, g := range GREASEs {
|
|
||||||
if a == g {
|
|
||||||
eqCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if eqCount > 40 {
|
|
||||||
t.Error("GREASE is not random", GREASEs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//func TestChromeJA3(t *testing.T) {
|
|
||||||
// result := common.AddRecordLayer((&Chrome{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
|
|
||||||
// assert.Equal(t, 517, len(result))
|
|
||||||
//
|
|
||||||
// hello := tlsx.ClientHelloBasic{}
|
|
||||||
// err := hello.Unmarshal(result)
|
|
||||||
// assert.Nil(t, err)
|
|
||||||
//
|
|
||||||
// // Chrome shuffles the order of extensions, so it needs special handling
|
|
||||||
// full := string(ja3.Bare(&hello))
|
|
||||||
// // TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
|
|
||||||
// parts := strings.Split(full, ",")
|
|
||||||
//
|
|
||||||
// // TLSVersion,Ciphers
|
|
||||||
// assert.Equal(t,
|
|
||||||
// []string{
|
|
||||||
// "771",
|
|
||||||
// "4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53",
|
|
||||||
// }, parts[0:2])
|
|
||||||
// // EllipticCurves,EllipticCurvePointFormats
|
|
||||||
// assert.Equal(t,
|
|
||||||
// []string{
|
|
||||||
// "29-23-24", "0",
|
|
||||||
// }, parts[3:5])
|
|
||||||
//
|
|
||||||
// normaliseExtensions := func(extensions string) []string {
|
|
||||||
// extensionParts := strings.Split(parts[2], "-")
|
|
||||||
// sort.Strings(extensionParts)
|
|
||||||
// return extensionParts
|
|
||||||
// }
|
|
||||||
// assert.Equal(t, normaliseExtensions("10-5-45-0-17513-13-18-11-23-16-35-27-65281-43-51-21"), normaliseExtensions(parts[2]))
|
|
||||||
//}
|
|
||||||
|
|
@ -21,8 +21,10 @@ func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.D
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i := 0; i < connConfig.NumConn; i++ {
|
for i := 0; i < connConfig.NumConn; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
transportConfig := connConfig.Transport
|
||||||
go func() {
|
go func() {
|
||||||
makeconn:
|
makeconn:
|
||||||
|
transportConn := transportConfig.CreateTransport()
|
||||||
remoteConn, err := dialer.Dial("tcp", connConfig.RemoteAddr)
|
remoteConn, err := dialer.Dial("tcp", connConfig.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to establish new connections to remote: %v", err)
|
log.Errorf("Failed to establish new connections to remote: %v", err)
|
||||||
|
|
@ -31,12 +33,20 @@ func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.D
|
||||||
goto makeconn
|
goto makeconn
|
||||||
}
|
}
|
||||||
|
|
||||||
transportConn := connConfig.TransportMaker()
|
|
||||||
sk, err := transportConn.Handshake(remoteConn, authInfo)
|
sk, err := transportConn.Handshake(remoteConn, authInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
transportConn.Close()
|
|
||||||
log.Errorf("Failed to prepare connection to remote: %v", err)
|
log.Errorf("Failed to prepare connection to remote: %v", err)
|
||||||
|
transportConn.Close()
|
||||||
|
|
||||||
|
// In Cloak v2.11.0, we've updated uTLS version and subsequently increased the first packet size for chrome above 1500
|
||||||
|
// https://github.com/cbeuw/Cloak/pull/306#issuecomment-2862728738. As a backwards compatibility feature, if we fail
|
||||||
|
// to connect using chrome signature, retry with firefox which has a smaller packet size.
|
||||||
|
if transportConfig.mode == "direct" && transportConfig.browser == chrome {
|
||||||
|
transportConfig.browser = firefox
|
||||||
|
log.Warnf("failed to connect with chrome signature, falling back to retry with firefox")
|
||||||
|
}
|
||||||
time.Sleep(time.Second * 3)
|
time.Sleep(time.Second * 3)
|
||||||
|
|
||||||
goto makeconn
|
goto makeconn
|
||||||
}
|
}
|
||||||
// sessionKey given by each connection should be identical
|
// sessionKey given by each connection should be identical
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// Fingerprint of Firefox 112
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"github.com/cbeuw/Cloak/internal/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Firefox struct{}
|
|
||||||
|
|
||||||
func (f *Firefox) composeExtensions(serverName string, keyShare []byte) []byte {
|
|
||||||
composeKeyShare := func(hidden []byte) []byte {
|
|
||||||
ret := make([]byte, 107)
|
|
||||||
ret[0], ret[1] = 0x00, 0x69 // length 105
|
|
||||||
ret[2], ret[3] = 0x00, 0x1d // group x25519
|
|
||||||
ret[4], ret[5] = 0x00, 0x20 // length 32
|
|
||||||
copy(ret[6:38], hidden)
|
|
||||||
ret[38], ret[39] = 0x00, 0x17 // group secp256r1
|
|
||||||
ret[40], ret[41] = 0x00, 0x41 // length 65
|
|
||||||
common.CryptoRandRead(ret[42:107])
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
// extension length is always 401, and server name length is variable
|
|
||||||
var ext [13][]byte
|
|
||||||
ext[0] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
|
|
||||||
ext[1] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
|
|
||||||
ext[2] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
|
|
||||||
ext[3] = addExtRec([]byte{0x00, 0x0a}, decodeHex("000c001d00170018001901000101")) // supported groups
|
|
||||||
ext[4] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
|
|
||||||
ext[5] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
|
|
||||||
ext[6] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
|
|
||||||
ext[7] = addExtRec([]byte{0x00, 0x22}, decodeHex("00080403050306030203")) // delegated credentials
|
|
||||||
ext[8] = addExtRec([]byte{0x00, 0x33}, composeKeyShare(keyShare)) // key share
|
|
||||||
ext[9] = addExtRec([]byte{0x00, 0x2b}, decodeHex("0403040303")) // supported versions
|
|
||||||
ext[10] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030503060308040805080604010501060102030201")) // Signature Algorithms
|
|
||||||
ext[11] = addExtRec([]byte{0x00, 0x1c}, []byte{0x40, 0x01}) // record size limit
|
|
||||||
// len(ext[0]) + len(all other ext) + len(len field of padding) + len(padding) = 401
|
|
||||||
// len(all other ext) = 228
|
|
||||||
// len(len field of padding) = 4
|
|
||||||
// len(padding) = 169 - len(ext[0])
|
|
||||||
ext[12] = addExtRec([]byte{0x00, 0x15}, make([]byte, 169-len(ext[0]))) // padding
|
|
||||||
var ret []byte
|
|
||||||
for _, e := range ext {
|
|
||||||
ret = append(ret, e...)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firefox) composeClientHello(hd clientHelloFields) (ch []byte) {
|
|
||||||
var clientHello [12][]byte
|
|
||||||
clientHello[0] = []byte{0x01} // handshake type
|
|
||||||
clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508
|
|
||||||
clientHello[2] = []byte{0x03, 0x03} // client version
|
|
||||||
clientHello[3] = hd.random // random
|
|
||||||
clientHello[4] = []byte{0x20} // session id length 32
|
|
||||||
clientHello[5] = hd.sessionId // session id
|
|
||||||
clientHello[6] = []byte{0x00, 0x22} // cipher suites length 34
|
|
||||||
clientHello[7] = decodeHex("130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035") // cipher suites
|
|
||||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
|
||||||
clientHello[9] = []byte{0x00} // compression methods
|
|
||||||
|
|
||||||
extensions := f.composeExtensions(hd.serverName, hd.x25519KeyShare)
|
|
||||||
clientHello[10] = []byte{0x00, 0x00}
|
|
||||||
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
|
|
||||||
clientHello[11] = extensions
|
|
||||||
|
|
||||||
var ret []byte
|
|
||||||
for _, c := range clientHello {
|
|
||||||
ret = append(ret, c...)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var hd = clientHelloFields{
|
|
||||||
random: decodeHex("ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a0"),
|
|
||||||
sessionId: decodeHex("47485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b9"),
|
|
||||||
x25519KeyShare: decodeHex("d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa44"),
|
|
||||||
serverName: "github.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
//func TestFirefoxJA3(t *testing.T) {
|
|
||||||
// result := common.AddRecordLayer((&Firefox{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
|
|
||||||
//
|
|
||||||
// hello := tlsx.ClientHelloBasic{}
|
|
||||||
// err := hello.Unmarshal(result)
|
|
||||||
// assert.Nil(t, err)
|
|
||||||
//
|
|
||||||
// digest := ja3.DigestHex(&hello)
|
|
||||||
// assert.Equal(t, "ad55557b7cbd735c2627f7ebb3b3d493", digest)
|
|
||||||
//}
|
|
||||||
|
|
||||||
func TestFirefoxComposeClientHello(t *testing.T) {
|
|
||||||
result := hex.EncodeToString((&Firefox{}).composeClientHello(hd))
|
|
||||||
target := "010001fc0303ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a02047485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b90022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035010001910000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b000201000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d0020d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa440017004104c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47002b00050403040303000d0018001604030503060308040805080604010501060102030201001c0002400100150096000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
||||||
|
|
||||||
// skip random secp256r1
|
|
||||||
secp256r1 := "04c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47"
|
|
||||||
start := strings.Index(target, secp256r1)
|
|
||||||
|
|
||||||
target = strings.Replace(target, secp256r1, "", 1)
|
|
||||||
result = strings.Replace(result, result[start:start+len(secp256r1)], "", 1)
|
|
||||||
|
|
||||||
assert.Equal(t, target, result)
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
// Fingerprint of Safari 16.4
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Safari struct{}
|
|
||||||
|
|
||||||
func (s *Safari) composeExtensions(serverName string, keyShare []byte) []byte {
|
|
||||||
makeSupportedGroups := func() []byte {
|
|
||||||
suppGroupListLen := []byte{0x00, 0x0a}
|
|
||||||
ret := make([]byte, 2+2+8)
|
|
||||||
copy(ret[0:2], suppGroupListLen)
|
|
||||||
copy(ret[2:4], makeGREASE())
|
|
||||||
copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19})
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
makeKeyShare := func(hidden []byte) []byte {
|
|
||||||
ret := make([]byte, 43)
|
|
||||||
ret[0], ret[1] = 0x00, 0x29 // length 41
|
|
||||||
copy(ret[2:4], makeGREASE())
|
|
||||||
ret[4], ret[5] = 0x00, 0x01 // length 1
|
|
||||||
ret[6] = 0x00
|
|
||||||
ret[7], ret[8] = 0x00, 0x1d // group x25519
|
|
||||||
ret[9], ret[10] = 0x00, 0x20 // length 32
|
|
||||||
copy(ret[11:43], hidden)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// extension length is always 393, and server name length is variable
|
|
||||||
var ext [16][]byte
|
|
||||||
ext[0] = addExtRec(makeGREASE(), nil) // First GREASE
|
|
||||||
ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
|
|
||||||
ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
|
|
||||||
ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
|
|
||||||
ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups
|
|
||||||
ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
|
|
||||||
ext[6] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
|
|
||||||
ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
|
|
||||||
ext[8] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030804040105030203080508050501080606010201")) // Signature Algorithms
|
|
||||||
ext[9] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp
|
|
||||||
ext[10] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share
|
|
||||||
ext[11] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
|
|
||||||
suppVersions := decodeHex("0a5a5a0304030303020301") // 5a5a needs to be a GREASE
|
|
||||||
copy(suppVersions[1:3], makeGREASE())
|
|
||||||
ext[12] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
|
|
||||||
ext[13] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x01}) // compress certificate
|
|
||||||
ext[14] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE
|
|
||||||
// len(ext[1]) + len(all other ext) + len(ext[15]) = 393
|
|
||||||
// len(all other ext) = 174
|
|
||||||
// len(ext[15]) = 219 - len(ext[1])
|
|
||||||
// 2+2+len(padding) = 219 - len(ext[1])
|
|
||||||
// len(padding) = 215 - len(ext[1])
|
|
||||||
ext[15] = addExtRec([]byte{0x00, 0x15}, make([]byte, 215-len(ext[1]))) // padding
|
|
||||||
var ret []byte
|
|
||||||
for _, e := range ext {
|
|
||||||
ret = append(ret, e...)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Safari) composeClientHello(hd clientHelloFields) (ch []byte) {
|
|
||||||
var clientHello [12][]byte
|
|
||||||
clientHello[0] = []byte{0x01} // handshake type
|
|
||||||
clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508
|
|
||||||
clientHello[2] = []byte{0x03, 0x03} // client version
|
|
||||||
clientHello[3] = hd.random // random
|
|
||||||
clientHello[4] = []byte{0x20} // session id length 32
|
|
||||||
clientHello[5] = hd.sessionId // session id
|
|
||||||
clientHello[6] = []byte{0x00, 0x2a} // cipher suites length 42
|
|
||||||
clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a")...) // cipher suites
|
|
||||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
|
||||||
clientHello[9] = []byte{0x00} // compression methods
|
|
||||||
|
|
||||||
extensions := s.composeExtensions(hd.serverName, hd.x25519KeyShare)
|
|
||||||
clientHello[10] = []byte{0x00, 0x00}
|
|
||||||
binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length
|
|
||||||
clientHello[11] = extensions
|
|
||||||
|
|
||||||
var ret []byte
|
|
||||||
for _, c := range clientHello {
|
|
||||||
ret = append(ret, c...)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var safariHd = clientHelloFields{
|
|
||||||
random: decodeHex("977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc17"),
|
|
||||||
sessionId: decodeHex("c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3"),
|
|
||||||
x25519KeyShare: decodeHex("c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a"),
|
|
||||||
serverName: "github.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
//func TestSafariJA3(t *testing.T) {
|
|
||||||
// result := common.AddRecordLayer((&Safari{}).composeClientHello(safariHd), common.Handshake, common.VersionTLS11)
|
|
||||||
//
|
|
||||||
// hello := tlsx.ClientHelloBasic{}
|
|
||||||
// err := hello.Unmarshal(result)
|
|
||||||
// assert.Nil(t, err)
|
|
||||||
//
|
|
||||||
// digest := ja3.DigestHex(&hello)
|
|
||||||
// assert.Equal(t, "773906b0efdefa24a7f2b8eb6985bf37", digest)
|
|
||||||
//}
|
|
||||||
|
|
||||||
func TestSafariComposeClientHello(t *testing.T) {
|
|
||||||
result := (&Safari{}).composeClientHello(safariHd)
|
|
||||||
target := decodeHex("010001fc0303977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc1720c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3002acaca130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000189fafa00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000c000a7a7a001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d0018001604030804040105030203080508050501080606010201001200000033002b00297a7a000100001d0020c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a002d00020101002b000b0a2a2a0304030303020301001b00030200017a7a000100001500c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
|
||||||
for p := 0; p < len(result); p++ {
|
|
||||||
if result[p] != target[p] {
|
|
||||||
if result[p]&0x0F == 0xA && target[p]&0x0F == 0xA &&
|
|
||||||
((p > 0 && result[p-1] == result[p] && target[p-1] == target[p]) ||
|
|
||||||
(p < len(result)-1 && result[p+1] == result[p] && target[p+1] == target[p])) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.Errorf("inequality at %v", p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -43,11 +43,11 @@ type RawConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoteConnConfig struct {
|
type RemoteConnConfig struct {
|
||||||
Singleplex bool
|
Singleplex bool
|
||||||
NumConn int
|
NumConn int
|
||||||
KeepAlive time.Duration
|
KeepAlive time.Duration
|
||||||
RemoteAddr string
|
RemoteAddr string
|
||||||
TransportMaker func() Transport
|
Transport TransportConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalConnConfig struct {
|
type LocalConnConfig struct {
|
||||||
|
|
@ -230,10 +230,9 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
|
||||||
raw.CDNWsUrlPath = "/"
|
raw.CDNWsUrlPath = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
remote.TransportMaker = func() Transport {
|
remote.Transport = TransportConfig{
|
||||||
return &WSOverTLS{
|
mode: "cdn",
|
||||||
wsUrl: "ws://" + cdnDomainPort + raw.CDNWsUrlPath,
|
wsUrl: "ws://" + cdnDomainPort + raw.CDNWsUrlPath,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case "direct":
|
case "direct":
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|
@ -241,18 +240,17 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
|
||||||
var browser browser
|
var browser browser
|
||||||
switch strings.ToLower(raw.BrowserSig) {
|
switch strings.ToLower(raw.BrowserSig) {
|
||||||
case "firefox":
|
case "firefox":
|
||||||
browser = &Firefox{}
|
browser = firefox
|
||||||
case "safari":
|
case "safari":
|
||||||
browser = &Safari{}
|
browser = safari
|
||||||
case "chrome":
|
case "chrome":
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
browser = &Chrome{}
|
browser = chrome
|
||||||
}
|
}
|
||||||
remote.TransportMaker = func() Transport {
|
remote.Transport = TransportConfig{
|
||||||
return &DirectTLS{
|
mode: "direct",
|
||||||
browser: browser,
|
browser: browser,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,26 @@ type Transport interface {
|
||||||
Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error)
|
Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error)
|
||||||
net.Conn
|
net.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransportConfig struct {
|
||||||
|
mode string
|
||||||
|
|
||||||
|
wsUrl string
|
||||||
|
|
||||||
|
browser browser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TransportConfig) CreateTransport() Transport {
|
||||||
|
switch t.mode {
|
||||||
|
case "cdn":
|
||||||
|
return &WSOverTLS{
|
||||||
|
wsUrl: t.wsUrl,
|
||||||
|
}
|
||||||
|
case "direct":
|
||||||
|
return &DirectTLS{
|
||||||
|
browser: t.browser,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/cbeuw/Cloak/internal/common"
|
"github.com/cbeuw/Cloak/internal/common"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
utls "gitlab.com/yawning/utls.git"
|
utls "github.com/refraction-networking/utls"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WSOverTLS struct {
|
type WSOverTLS struct {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
@ -52,8 +53,8 @@ func CryptoRandRead(buf []byte) {
|
||||||
RandRead(rand.Reader, buf)
|
RandRead(rand.Reader, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RandRead(randSource io.Reader, buf []byte) {
|
func backoff(f func() error) {
|
||||||
_, err := randSource.Read(buf)
|
err := f()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -61,12 +62,36 @@ func RandRead(randSource io.Reader, buf []byte) {
|
||||||
100 * time.Millisecond, 300 * time.Millisecond, 500 * time.Millisecond, 1 * time.Second,
|
100 * time.Millisecond, 300 * time.Millisecond, 500 * time.Millisecond, 1 * time.Second,
|
||||||
3 * time.Second, 5 * time.Second}
|
3 * time.Second, 5 * time.Second}
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
log.Errorf("Failed to get random bytes: %v. Retrying...", err)
|
log.Errorf("Failed to get random: %v. Retrying...", err)
|
||||||
_, err = randSource.Read(buf)
|
err = f()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(waitDur[i])
|
time.Sleep(waitDur[i])
|
||||||
}
|
}
|
||||||
log.Fatal("Cannot get random bytes after 10 retries")
|
log.Fatal("Cannot get random after 10 retries")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandRead(randSource io.Reader, buf []byte) {
|
||||||
|
backoff(func() error {
|
||||||
|
_, err := randSource.Read(buf)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandItem[T any](list []T) T {
|
||||||
|
return list[RandInt(len(list))]
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandInt(n int) int {
|
||||||
|
s := new(int)
|
||||||
|
backoff(func() error {
|
||||||
|
size, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = int(size.Int64())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return *s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,46 +66,6 @@ func (d *datagramBufferedPipe) Read(target []byte) (int, error) {
|
||||||
return dataLen, nil
|
return dataLen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *datagramBufferedPipe) WriteTo(w io.Writer) (n int64, err error) {
|
|
||||||
d.rwCond.L.Lock()
|
|
||||||
defer d.rwCond.L.Unlock()
|
|
||||||
for {
|
|
||||||
if d.closed && len(d.pLens) == 0 {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRDeadline := !d.rDeadline.IsZero()
|
|
||||||
if hasRDeadline {
|
|
||||||
if time.Until(d.rDeadline) <= 0 {
|
|
||||||
return 0, ErrTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.pLens) > 0 {
|
|
||||||
var dataLen int
|
|
||||||
dataLen, d.pLens = d.pLens[0], d.pLens[1:]
|
|
||||||
written, er := w.Write(d.buf.Next(dataLen))
|
|
||||||
n += int64(written)
|
|
||||||
if er != nil {
|
|
||||||
d.rwCond.Broadcast()
|
|
||||||
return n, er
|
|
||||||
}
|
|
||||||
d.rwCond.Broadcast()
|
|
||||||
} else {
|
|
||||||
if d.wtTimeout == 0 {
|
|
||||||
if hasRDeadline {
|
|
||||||
d.broadcastAfter(time.Until(d.rDeadline))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
d.rDeadline = time.Now().Add(d.wtTimeout)
|
|
||||||
d.broadcastAfter(d.wtTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.rwCond.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *datagramBufferedPipe) Write(f *Frame) (toBeClosed bool, err error) {
|
func (d *datagramBufferedPipe) Write(f *Frame) (toBeClosed bool, err error) {
|
||||||
d.rwCond.L.Lock()
|
d.rwCond.L.Lock()
|
||||||
defer d.rwCond.L.Unlock()
|
defer d.rwCond.L.Unlock()
|
||||||
|
|
@ -151,14 +111,6 @@ func (d *datagramBufferedPipe) SetReadDeadline(t time.Time) {
|
||||||
d.rwCond.Broadcast()
|
d.rwCond.Broadcast()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *datagramBufferedPipe) SetWriteToTimeout(t time.Duration) {
|
|
||||||
d.rwCond.L.Lock()
|
|
||||||
defer d.rwCond.L.Unlock()
|
|
||||||
|
|
||||||
d.wtTimeout = t
|
|
||||||
d.rwCond.Broadcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *datagramBufferedPipe) broadcastAfter(t time.Duration) {
|
func (d *datagramBufferedPipe) broadcastAfter(t time.Duration) {
|
||||||
if d.timeoutTimer != nil {
|
if d.timeoutTimer != nil {
|
||||||
d.timeoutTimer.Stop()
|
d.timeoutTimer.Stop()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,8 @@ type recvBuffer interface {
|
||||||
// Instead, it should behave as if it hasn't been closed. Closure is only relevant
|
// Instead, it should behave as if it hasn't been closed. Closure is only relevant
|
||||||
// when the buffer is empty.
|
// when the buffer is empty.
|
||||||
io.ReadCloser
|
io.ReadCloser
|
||||||
io.WriterTo
|
|
||||||
Write(*Frame) (toBeClosed bool, err error)
|
Write(*Frame) (toBeClosed bool, err error)
|
||||||
SetReadDeadline(time time.Time)
|
SetReadDeadline(time time.Time)
|
||||||
// SetWriteToTimeout sets the duration a recvBuffer waits in a WriteTo call when nothing
|
|
||||||
// has been written for a while. After that duration it should return ErrTimeout
|
|
||||||
SetWriteToTimeout(d time.Duration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// size we want the amount of unread data in buffer to grow before recvBuffer.Write blocks.
|
// size we want the amount of unread data in buffer to grow before recvBuffer.Write blocks.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -265,6 +265,7 @@ func (sesh *Session) recvDataFromRemote(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sesh *Session) SetTerminalMsg(msg string) {
|
func (sesh *Session) SetTerminalMsg(msg string) {
|
||||||
|
log.Debug("terminal message set to " + msg)
|
||||||
sesh.terminalMsgSetter.Do(func() {
|
sesh.terminalMsgSetter.Do(func() {
|
||||||
sesh.terminalMsg = msg
|
sesh.terminalMsg = msg
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -557,7 +557,7 @@ func BenchmarkRecvDataFromRemote(b *testing.B) {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stream, _ := sesh.Accept()
|
stream, _ := sesh.Accept()
|
||||||
stream.(*Stream).WriteTo(ioutil.Discard)
|
io.Copy(ioutil.Discard, stream)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
binaryFrames := [maxIter][]byte{}
|
binaryFrames := [maxIter][]byte{}
|
||||||
|
|
|
||||||
|
|
@ -96,17 +96,6 @@ func (s *Stream) Read(buf []byte) (n int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTo continuously write data Stream has received into the writer w.
|
|
||||||
func (s *Stream) WriteTo(w io.Writer) (int64, error) {
|
|
||||||
// will keep writing until the underlying buffer is closed
|
|
||||||
n, err := s.recvBuf.WriteTo(w)
|
|
||||||
log.Tracef("%v read from stream %v with err %v", n, s.id, err)
|
|
||||||
if err == io.EOF {
|
|
||||||
return n, ErrBrokenStream
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) obfuscateAndSend(buf []byte, payloadOffsetInBuf int) error {
|
func (s *Stream) obfuscateAndSend(buf []byte, payloadOffsetInBuf int) error {
|
||||||
cipherTextLen, err := s.session.obfuscate(&s.writingFrame, buf, payloadOffsetInBuf)
|
cipherTextLen, err := s.session.obfuscate(&s.writingFrame, buf, payloadOffsetInBuf)
|
||||||
s.writingFrame.Seq++
|
s.writingFrame.Seq++
|
||||||
|
|
@ -210,7 +199,6 @@ func (s *Stream) Close() error {
|
||||||
func (s *Stream) LocalAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[0] }
|
func (s *Stream) LocalAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[0] }
|
||||||
func (s *Stream) RemoteAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[1] }
|
func (s *Stream) RemoteAddr() net.Addr { return s.session.addrs.Load().([]net.Addr)[1] }
|
||||||
|
|
||||||
func (s *Stream) SetWriteToTimeout(d time.Duration) { s.recvBuf.SetWriteToTimeout(d) }
|
|
||||||
func (s *Stream) SetReadDeadline(t time.Time) error { s.recvBuf.SetReadDeadline(t); return nil }
|
func (s *Stream) SetReadDeadline(t time.Time) error { s.recvBuf.SetReadDeadline(t); return nil }
|
||||||
func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d }
|
func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ package multiplex
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
"container/heap"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -102,10 +101,6 @@ func (sb *streamBuffer) Read(buf []byte) (int, error) {
|
||||||
return sb.buf.Read(buf)
|
return sb.buf.Read(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *streamBuffer) WriteTo(w io.Writer) (int64, error) {
|
|
||||||
return sb.buf.WriteTo(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sb *streamBuffer) Close() error {
|
func (sb *streamBuffer) Close() error {
|
||||||
sb.recvM.Lock()
|
sb.recvM.Lock()
|
||||||
defer sb.recvM.Unlock()
|
defer sb.recvM.Unlock()
|
||||||
|
|
@ -113,5 +108,4 @@ func (sb *streamBuffer) Close() error {
|
||||||
return sb.buf.Close()
|
return sb.buf.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *streamBuffer) SetReadDeadline(t time.Time) { sb.buf.SetReadDeadline(t) }
|
func (sb *streamBuffer) SetReadDeadline(t time.Time) { sb.buf.SetReadDeadline(t) }
|
||||||
func (sb *streamBuffer) SetWriteToTimeout(d time.Duration) { sb.buf.SetWriteToTimeout(d) }
|
|
||||||
|
|
|
||||||
|
|
@ -58,43 +58,6 @@ func (p *streamBufferedPipe) Read(target []byte) (int, error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *streamBufferedPipe) WriteTo(w io.Writer) (n int64, err error) {
|
|
||||||
p.rwCond.L.Lock()
|
|
||||||
defer p.rwCond.L.Unlock()
|
|
||||||
for {
|
|
||||||
if p.closed && p.buf.Len() == 0 {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRDeadline := !p.rDeadline.IsZero()
|
|
||||||
if hasRDeadline {
|
|
||||||
if time.Until(p.rDeadline) <= 0 {
|
|
||||||
return 0, ErrTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if p.buf.Len() > 0 {
|
|
||||||
written, er := p.buf.WriteTo(w)
|
|
||||||
n += written
|
|
||||||
if er != nil {
|
|
||||||
p.rwCond.Broadcast()
|
|
||||||
return n, er
|
|
||||||
}
|
|
||||||
p.rwCond.Broadcast()
|
|
||||||
} else {
|
|
||||||
if p.wtTimeout == 0 {
|
|
||||||
if hasRDeadline {
|
|
||||||
p.broadcastAfter(time.Until(p.rDeadline))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.rDeadline = time.Now().Add(p.wtTimeout)
|
|
||||||
p.broadcastAfter(p.wtTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.rwCond.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *streamBufferedPipe) Write(input []byte) (int, error) {
|
func (p *streamBufferedPipe) Write(input []byte) (int, error) {
|
||||||
p.rwCond.L.Lock()
|
p.rwCond.L.Lock()
|
||||||
defer p.rwCond.L.Unlock()
|
defer p.rwCond.L.Unlock()
|
||||||
|
|
@ -131,14 +94,6 @@ func (p *streamBufferedPipe) SetReadDeadline(t time.Time) {
|
||||||
p.rwCond.Broadcast()
|
p.rwCond.Broadcast()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *streamBufferedPipe) SetWriteToTimeout(d time.Duration) {
|
|
||||||
p.rwCond.L.Lock()
|
|
||||||
defer p.rwCond.L.Unlock()
|
|
||||||
|
|
||||||
p.wtTimeout = d
|
|
||||||
p.rwCond.Broadcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *streamBufferedPipe) broadcastAfter(d time.Duration) {
|
func (p *streamBufferedPipe) broadcastAfter(d time.Duration) {
|
||||||
if p.timeoutTimer != nil {
|
if p.timeoutTimer != nil {
|
||||||
p.timeoutTimer.Stop()
|
p.timeoutTimer.Stop()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package multiplex
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -364,31 +363,6 @@ func TestStream_Read(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStream_SetWriteToTimeout(t *testing.T) {
|
|
||||||
seshes := map[string]*Session{
|
|
||||||
"ordered": setupSesh(false, emptyKey, EncryptionMethodPlain),
|
|
||||||
"unordered": setupSesh(true, emptyKey, EncryptionMethodPlain),
|
|
||||||
}
|
|
||||||
for name, sesh := range seshes {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
stream, _ := sesh.OpenStream()
|
|
||||||
stream.SetWriteToTimeout(100 * time.Millisecond)
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
stream.WriteTo(ioutil.Discard)
|
|
||||||
done <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
t.Error("didn't timeout")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStream_SetReadFromTimeout(t *testing.T) {
|
func TestStream_SetReadFromTimeout(t *testing.T) {
|
||||||
seshes := map[string]*Session{
|
seshes := map[string]*Session{
|
||||||
"ordered": setupSesh(false, emptyKey, EncryptionMethodPlain),
|
"ordered": setupSesh(false, emptyKey, EncryptionMethodPlain),
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@ package multiplex
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"github.com/cbeuw/Cloak/internal/common"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type switchboardStrategy int
|
type switchboardStrategy int
|
||||||
|
|
@ -39,19 +38,14 @@ type switchboard struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSwitchboard(sesh *Session) *switchboard {
|
func makeSwitchboard(sesh *Session) *switchboard {
|
||||||
var strategy switchboardStrategy
|
|
||||||
if sesh.Unordered {
|
|
||||||
log.Debug("Connection is unordered")
|
|
||||||
strategy = uniformSpread
|
|
||||||
} else {
|
|
||||||
strategy = fixedConnMapping
|
|
||||||
}
|
|
||||||
sb := &switchboard{
|
sb := &switchboard{
|
||||||
session: sesh,
|
session: sesh,
|
||||||
strategy: strategy,
|
strategy: uniformSpread,
|
||||||
valve: sesh.Valve,
|
valve: sesh.Valve,
|
||||||
randPool: sync.Pool{New: func() interface{} {
|
randPool: sync.Pool{New: func() interface{} {
|
||||||
return rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
var state [32]byte
|
||||||
|
common.CryptoRandRead(state[:])
|
||||||
|
return rand.New(rand.NewChaCha8(state))
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
return sb
|
return sb
|
||||||
|
|
@ -60,8 +54,8 @@ func makeSwitchboard(sesh *Session) *switchboard {
|
||||||
var errBrokenSwitchboard = errors.New("the switchboard is broken")
|
var errBrokenSwitchboard = errors.New("the switchboard is broken")
|
||||||
|
|
||||||
func (sb *switchboard) addConn(conn net.Conn) {
|
func (sb *switchboard) addConn(conn net.Conn) {
|
||||||
atomic.AddUint32(&sb.connsCount, 1)
|
connId := atomic.AddUint32(&sb.connsCount, 1) - 1
|
||||||
sb.conns.Store(conn, conn)
|
sb.conns.Store(connId, conn)
|
||||||
go sb.deplex(conn)
|
go sb.deplex(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,6 +80,9 @@ func (sb *switchboard) send(data []byte, assignedConn *net.Conn) (n int, err err
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
case fixedConnMapping:
|
case fixedConnMapping:
|
||||||
|
// FIXME: this strategy has a tendency to cause a TLS conn socket buffer to fill up,
|
||||||
|
// which is a problem when multiple streams are mapped to the same conn, resulting
|
||||||
|
// in all such streams being blocked.
|
||||||
conn = *assignedConn
|
conn = *assignedConn
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
conn, err = sb.pickRandConn()
|
conn, err = sb.pickRandConn()
|
||||||
|
|
@ -110,7 +107,7 @@ func (sb *switchboard) send(data []byte, assignedConn *net.Conn) (n int, err err
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a random connId
|
// returns a random conn. This function can be called concurrently.
|
||||||
func (sb *switchboard) pickRandConn() (net.Conn, error) {
|
func (sb *switchboard) pickRandConn() (net.Conn, error) {
|
||||||
if atomic.LoadUint32(&sb.broken) == 1 {
|
if atomic.LoadUint32(&sb.broken) == 1 {
|
||||||
return nil, errBrokenSwitchboard
|
return nil, errBrokenSwitchboard
|
||||||
|
|
@ -122,22 +119,15 @@ func (sb *switchboard) pickRandConn() (net.Conn, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
randReader := sb.randPool.Get().(*rand.Rand)
|
randReader := sb.randPool.Get().(*rand.Rand)
|
||||||
|
connId := randReader.Uint32N(connsCount)
|
||||||
r := randReader.Intn(int(connsCount))
|
|
||||||
sb.randPool.Put(randReader)
|
sb.randPool.Put(randReader)
|
||||||
|
|
||||||
var c int
|
ret, ok := sb.conns.Load(connId)
|
||||||
var ret net.Conn
|
if !ok {
|
||||||
sb.conns.Range(func(_, conn interface{}) bool {
|
log.Errorf("failed to get conn %d", connId)
|
||||||
if r == c {
|
return nil, errBrokenSwitchboard
|
||||||
ret = conn.(net.Conn)
|
}
|
||||||
return false
|
return ret.(net.Conn), nil
|
||||||
}
|
|
||||||
c++
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// actively triggered by session.Close()
|
// actively triggered by session.Close()
|
||||||
|
|
@ -145,10 +135,10 @@ func (sb *switchboard) closeAll() {
|
||||||
if !atomic.CompareAndSwapUint32(&sb.broken, 0, 1) {
|
if !atomic.CompareAndSwapUint32(&sb.broken, 0, 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
atomic.StoreUint32(&sb.connsCount, 0)
|
||||||
sb.conns.Range(func(_, conn interface{}) bool {
|
sb.conns.Range(func(_, conn interface{}) bool {
|
||||||
conn.(net.Conn).Close()
|
conn.(net.Conn).Close()
|
||||||
sb.conns.Delete(conn)
|
sb.conns.Delete(conn)
|
||||||
atomic.AddUint32(&sb.connsCount, ^uint32(0))
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/cbeuw/Cloak/internal/common"
|
"github.com/cbeuw/Cloak/internal/common"
|
||||||
|
|
@ -46,8 +45,7 @@ func (TLS) makeResponder(clientHelloSessionId []byte, sharedSecret [32]byte) Res
|
||||||
// the cert length needs to be the same for all handshakes belonging to the same session
|
// the cert length needs to be the same for all handshakes belonging to the same session
|
||||||
// we can use sessionKey as a seed here to ensure consistency
|
// we can use sessionKey as a seed here to ensure consistency
|
||||||
possibleCertLengths := []int{42, 27, 68, 59, 36, 44, 46}
|
possibleCertLengths := []int{42, 27, 68, 59, 36, 44, 46}
|
||||||
rand.Seed(int64(sessionKey[0]))
|
cert := make([]byte, possibleCertLengths[common.RandInt(len(possibleCertLengths))])
|
||||||
cert := make([]byte, possibleCertLengths[rand.Intn(len(possibleCertLengths))])
|
|
||||||
common.RandRead(randSource, cert)
|
common.RandRead(randSource, cert)
|
||||||
|
|
||||||
var nonce [12]byte
|
var nonce [12]byte
|
||||||
|
|
|
||||||
|
|
@ -164,12 +164,12 @@ func parseClientHello(data []byte) (ret *ClientHello, err error) {
|
||||||
func composeServerHello(sessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte) []byte {
|
func composeServerHello(sessionId []byte, nonce [12]byte, encryptedSessionKeyWithTag [48]byte) []byte {
|
||||||
var serverHello [11][]byte
|
var serverHello [11][]byte
|
||||||
serverHello[0] = []byte{0x02} // handshake type
|
serverHello[0] = []byte{0x02} // handshake type
|
||||||
serverHello[1] = []byte{0x00, 0x00, 0x76} // length 77
|
serverHello[1] = []byte{0x00, 0x00, 0x76} // length 118
|
||||||
serverHello[2] = []byte{0x03, 0x03} // server version
|
serverHello[2] = []byte{0x03, 0x03} // server version
|
||||||
serverHello[3] = append(nonce[0:12], encryptedSessionKeyWithTag[0:20]...) // random 32 bytes
|
serverHello[3] = append(nonce[0:12], encryptedSessionKeyWithTag[0:20]...) // random 32 bytes
|
||||||
serverHello[4] = []byte{0x20} // session id length 32
|
serverHello[4] = []byte{0x20} // session id length 32
|
||||||
serverHello[5] = sessionId // session id
|
serverHello[5] = sessionId // session id
|
||||||
serverHello[6] = []byte{0xc0, 0x30} // cipher suite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
serverHello[6] = []byte{0x13, 0x02} // cipher suite TLS_AES_256_GCM_SHA384
|
||||||
serverHello[7] = []byte{0x00} // compression method null
|
serverHello[7] = []byte{0x00} // compression method null
|
||||||
serverHello[8] = []byte{0x00, 0x2e} // extensions length 46
|
serverHello[8] = []byte{0x00, 0x2e} // extensions length 46
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import (
|
||||||
|
|
||||||
var b64 = base64.StdEncoding.EncodeToString
|
var b64 = base64.StdEncoding.EncodeToString
|
||||||
|
|
||||||
|
const firstPacketSize = 3000
|
||||||
|
|
||||||
func Serve(l net.Listener, sta *State) {
|
func Serve(l net.Listener, sta *State) {
|
||||||
waitDur := [10]time.Duration{
|
waitDur := [10]time.Duration{
|
||||||
50 * time.Millisecond, 100 * time.Millisecond, 300 * time.Millisecond, 500 * time.Millisecond, 1 * time.Second,
|
50 * time.Millisecond, 100 * time.Millisecond, 300 * time.Millisecond, 500 * time.Millisecond, 1 * time.Second,
|
||||||
|
|
@ -124,7 +126,7 @@ func readFirstPacket(conn net.Conn, buf []byte, timeout time.Duration) (int, Tra
|
||||||
|
|
||||||
func dispatchConnection(conn net.Conn, sta *State) {
|
func dispatchConnection(conn net.Conn, sta *State) {
|
||||||
var err error
|
var err error
|
||||||
buf := make([]byte, 1500)
|
buf := make([]byte, firstPacketSize)
|
||||||
|
|
||||||
i, transport, redirOnErr, err := readFirstPacket(conn, buf, 15*time.Second)
|
i, transport, redirOnErr, err := readFirstPacket(conn, buf, 15*time.Second)
|
||||||
data := buf[:i]
|
data := buf[:i]
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,22 @@ func TestParseRedirAddr(t *testing.T) {
|
||||||
t.Errorf("parsing %v error: %v", domainNoPort, err)
|
t.Errorf("parsing %v error: %v", domainNoPort, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expHost, err := net.ResolveIPAddr("ip", "example.com")
|
|
||||||
|
expIPs, err := net.LookupIP("example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("tester error: cannot resolve example.com: %v", err)
|
t.Errorf("tester error: cannot resolve example.com: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if host.String() != expHost.String() {
|
|
||||||
t.Errorf("expected %v got %v", expHost.String(), host.String())
|
contain := false
|
||||||
|
for _, expIP := range expIPs {
|
||||||
|
if expIP.String() == host.String() {
|
||||||
|
contain = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contain {
|
||||||
|
t.Errorf("expected one of %v got %v", expIPs, host.String())
|
||||||
}
|
}
|
||||||
if port != "" {
|
if port != "" {
|
||||||
t.Errorf("port not empty when there is no port")
|
t.Errorf("port not empty when there is no port")
|
||||||
|
|
@ -63,13 +72,22 @@ func TestParseRedirAddr(t *testing.T) {
|
||||||
t.Errorf("parsing %v error: %v", domainWPort, err)
|
t.Errorf("parsing %v error: %v", domainWPort, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expHost, err := net.ResolveIPAddr("ip", "example.com")
|
|
||||||
|
expIPs, err := net.LookupIP("example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("tester error: cannot resolve example.com: %v", err)
|
t.Errorf("tester error: cannot resolve example.com: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if host.String() != expHost.String() {
|
|
||||||
t.Errorf("expected %v got %v", expHost.String(), host.String())
|
contain := false
|
||||||
|
for _, expIP := range expIPs {
|
||||||
|
if expIP.String() == host.String() {
|
||||||
|
contain = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contain {
|
||||||
|
t.Errorf("expected one of %v got %v", expIPs, host.String())
|
||||||
}
|
}
|
||||||
if port != "80" {
|
if port != "80" {
|
||||||
t.Errorf("wrong port: expected %v, got %v", "80", port)
|
t.Errorf("wrong port: expected %v, got %v", "80", port)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
go install github.com/mitchellh/gox@latest
|
go install github.com/mitchellh/gox@latest
|
||||||
|
|
||||||
mkdir -p release
|
mkdir -p release
|
||||||
|
|
@ -18,7 +20,7 @@ echo "Compiling:"
|
||||||
|
|
||||||
os="windows linux darwin"
|
os="windows linux darwin"
|
||||||
arch="amd64 386 arm arm64 mips mips64 mipsle mips64le"
|
arch="amd64 386 arm arm64 mips mips64 mipsle mips64le"
|
||||||
pushd cmd/ck-client || exit 1
|
pushd cmd/ck-client
|
||||||
CGO_ENABLED=0 gox -ldflags "-X main.version=${v}" -os="$os" -arch="$arch" -osarch="$osarch" -output="$output"
|
CGO_ENABLED=0 gox -ldflags "-X main.version=${v}" -os="$os" -arch="$arch" -osarch="$osarch" -output="$output"
|
||||||
CGO_ENABLED=0 GOOS="linux" GOARCH="mips" GOMIPS="softfloat" go build -ldflags "-X main.version=${v}" -o ck-client-linux-mips_softfloat-"${v}"
|
CGO_ENABLED=0 GOOS="linux" GOARCH="mips" GOMIPS="softfloat" go build -ldflags "-X main.version=${v}" -o ck-client-linux-mips_softfloat-"${v}"
|
||||||
CGO_ENABLED=0 GOOS="linux" GOARCH="mipsle" GOMIPS="softfloat" go build -ldflags "-X main.version=${v}" -o ck-client-linux-mipsle_softfloat-"${v}"
|
CGO_ENABLED=0 GOOS="linux" GOARCH="mipsle" GOMIPS="softfloat" go build -ldflags "-X main.version=${v}" -o ck-client-linux-mipsle_softfloat-"${v}"
|
||||||
|
|
@ -27,7 +29,7 @@ popd
|
||||||
|
|
||||||
os="linux"
|
os="linux"
|
||||||
arch="amd64 386 arm arm64"
|
arch="amd64 386 arm arm64"
|
||||||
pushd cmd/ck-server || exit 1
|
pushd cmd/ck-server
|
||||||
CGO_ENABLED=0 gox -ldflags "-X main.version=${v}" -os="$os" -arch="$arch" -osarch="$osarch" -output="$output"
|
CGO_ENABLED=0 gox -ldflags "-X main.version=${v}" -os="$os" -arch="$arch" -osarch="$osarch" -output="$output"
|
||||||
mv ck-server-* ../../release
|
mv ck-server-* ../../release
|
||||||
popd
|
popd
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"packagePatterns": ["*"],
|
||||||
|
"excludePackagePatterns": ["utls"],
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue