Compare commits

..

88 Commits

Author SHA1 Message Date
Andy Wang c3d5470ef7
Merge pull request #312 from cbeuw/renovate/github.com-refraction-networking-utls-1.x 2025-07-22 10:03:39 +01:00
renovate[bot] 8f629c7b2e
fix(deps): update module github.com/refraction-networking/utls to v1.8.0 2025-07-22 08:50:24 +00:00
Andy Wang 49357fc20b
Merge pull request #310 from cbeuw/utls-1.7.0
Update utls and increase server first packet buffer size
2025-06-08 18:21:55 +01:00
Andy Wang 8af137637e
Add backwards compatibility fallback to firefox 2025-06-08 18:19:58 +01:00
Andy Wang 51ed286f35
Update utls to 1.7.3 2025-06-08 18:19:57 +01:00
Andy Wang 5146ea8503
Increase server first packet buffer size to 3000 2025-06-08 18:19:48 +01:00
Andy Wang 7f9c17439f
Merge pull request #308 from cbeuw/deps-20250430
Update deps
2025-04-30 21:03:48 +01:00
Andy Wang c15fd730de
Update Go in CI 2025-04-30 21:00:54 +01:00
Andy Wang d06c208ace
Fix IP resolution in test 2025-04-30 20:57:23 +01:00
Andy Wang 9800e3685d
Update deps 2025-04-30 20:48:17 +01:00
Andy Wang 07aa197061
Go mod tidy 2025-04-30 20:39:09 +01:00
Andy Wang cfdd5e6560
Merge pull request #294 from zaferatli/docFix
chore: doc link fix
2025-04-30 20:35:14 +01:00
zaferatli 64166bf580 chore: doc link fix 2025-01-02 00:01:32 +03:00
Andy Wang d229d8b3dc
Merge pull request #283 from cbeuw/padding
Pad the first 5 frames to mitigate encapsulated TLS handshakes detection
2024-10-03 23:17:27 +01:00
Andy Wang 8bbc7b08d3
Fix tests 2024-10-03 23:06:42 +01:00
Andy Wang 5cf975f596
Pad the first 5 frames 2024-10-03 23:06:41 +01:00
Andy Wang 19c8cd1f89
Merge pull request #285 from cbeuw/docker-tests
Run compatibility tests against previous Cloak version
2024-10-03 23:04:58 +01:00
Andy Wang 5867fa932b
Merge pull request #281 from zestysoft/faliure_typo
Fix spelling mistake
2024-10-03 23:04:42 +01:00
Andy Wang bfaf46d2e9
Update actions 2024-10-03 22:54:48 +01:00
Andy Wang e362e81d19
Add backwards compatibility CI job 2024-10-03 22:54:48 +01:00
Andy Wang deb0d26c08
Build and push docker image on release 2024-10-01 21:48:42 +01:00
Ian Brown 3687087c67
Fix spelling mistake
Signed-off-by: Ian Brown <ian@zestysoft.com>
2024-09-20 19:45:46 -07:00
Andy Wang 97a03139bc
Merge pull request #262 from cbeuw/renovate/github.com-refraction-networking-utls-1.x
Update module github.com/refraction-networking/utls to v1.6.6
2024-06-16 13:19:04 +01:00
renovate[bot] b3c6426ac5
Update module github.com/refraction-networking/utls to v1.6.6 2024-05-03 23:07:04 +00:00
Andy Wang dc2e83f75f
Move to common.RandInt 2024-04-14 16:27:00 +01:00
Andy Wang 5988b4337d
Stop using fixedConnMapping 2024-04-14 16:25:54 +01:00
Andy Wang de4dab6bf3
Merge pull request #242 from notsure2/random-sni
Support ServerName randomization (by setting ServerName=random) using ProtonVPN algo
2024-04-13 23:11:19 +01:00
Andy Wang d5da5d049c
Update copyright 2024-04-13 23:10:09 +01:00
Andy Wang 392fc41de8
Move random utilities to common package 2024-04-13 23:08:34 +01:00
notsure2 3b449b64b3
Support ServerName randomization (by setting ServerName=random) using the same algorithm as ProtonVPN bcf344b39b 2024-04-13 22:48:55 +01:00
Andy Wang a848d2f7e5
Update go version and release script 2024-04-13 22:38:01 +01:00
Andy Wang de1c7600c1
Update dependencies 2024-04-11 20:54:52 +01:00
Andy Wang 767716b9be
Merge pull request #251 from cbeuw/renovate/github.com-refraction-networking-utls-1.x
Update module github.com/refraction-networking/utls to v1.6.4
2024-04-11 20:53:07 +01:00
renovate[bot] 1cc4a1f928
Update module github.com/refraction-networking/utls to v1.6.4 2024-04-11 19:49:31 +00:00
Andy Wang 82687d4419
Merge pull request #256 from BANanaD3V/master
Update go mod so it builds on nix
2024-04-11 20:48:56 +01:00
Nikita 6b08af0c18
Update go mod so it builds on nix 2024-03-10 08:43:45 +03:00
Andy Wang c48a8800d6
Remove old utls 2024-02-09 17:30:22 +00:00
renovate[bot] c5b31de753
Configure Renovate (#248)
* Add renovate.json

* Renovate only utls

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Andy Wang <cbeuw.andy@gmail.com>
2024-02-09 17:27:42 +00:00
Andy Wang b9907c2e18
Disable codecov check 2024-02-09 16:51:32 +00:00
Andy Wang 6417e3393d
Use utls for ClientHello fingerprint
Close #223
2024-02-09 16:37:57 +00:00
Andy Wang b3ec1ab3bc
Make server respond with a TLS 1.3 cipher suite 2024-02-09 15:49:42 +00:00
Andy Wang eca5f13936
Remove WriteTo from recvBuffer to prevent blocking on external Writer.
Fixes #229
2023-11-12 20:47:17 +00:00
Andy Wang fcb600efff
Print binary sha256 in release 2023-04-23 15:23:10 +02:00
Andy Wang 59919e5ec0
Remove gopacket dependency due to pcap 2023-04-23 15:14:14 +02:00
Andy Wang d04366ec32
Fix padding calculation 2023-04-23 15:09:35 +02:00
Andy Wang bc67074610
Add Safari browser signature 2023-04-23 11:04:02 +02:00
Andy Wang 641f6b2a9c
Update to Chrome and Firefox 112 2023-04-23 11:04:02 +02:00
Andy Wang 646a323065
Merge pull request #217 from cbeuw/dependabot/go_modules/golang.org/x/crypto-0.1.0
Bump golang.org/x/crypto from 0.0.0-20220131195533-30dcbda58838 to 0.1.0
2023-03-22 16:55:43 +00:00
dependabot[bot] b8f04c96c6
Bump golang.org/x/crypto from 0.0.0-20220131195533-30dcbda58838 to 0.1.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.0.0-20220131195533-30dcbda58838 to 0.1.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-22 16:37:25 +00:00
Andy Wang 0e0ec0e82b
Merge pull request #216 from githablocal/master
Make websocket url sub path configurable
2023-03-07 10:51:17 +00:00
name 6fe603c726 Fix gox not found in release CI 2023-03-07 17:17:19 +08:00
name 11e42dd542 Add WebSocket path for CDN mode
add new option to README

Change field name and default value of CDNWsUrlPath
2023-03-07 17:13:13 +08:00
name e696b18187 format using goimports 2023-03-07 17:09:18 +08:00
Andy Wang e305871d89
Merge pull request #186 from HirbodBehnam/master
Update browser fingerprints
2022-03-31 16:16:17 +01:00
HirbodBehnam 9614fbc03d Update firefox fingerprint 2022-03-24 14:14:50 +04:30
HirbodBehnam f67ae6e644 Update chrome fingerprint 2022-03-23 21:52:17 +04:30
Andy Wang 68f47f1d49
Merge pull request #184 from moonburnt/master 2022-03-20 23:13:31 +00:00
moonburnt 9e2549c117
Fix typo in README 2022-03-21 01:08:08 +02:00
Andy Wang 39306cf930
Update README.md 2022-02-25 00:21:37 +00:00
Andy Wang 8e07491e98
Update README.md 2022-02-24 19:38:38 +00:00
Andy Wang 847b7e24bf
Update dependencies 2022-02-02 22:18:34 +00:00
Andy Wang 750340126a
Update browser fingerprint versions (no actual fingerprint change) 2022-02-02 22:00:32 +00:00
Andy Wang 611bad91fd
Minor refactors to remove unnecessary function 2022-02-02 22:00:26 +00:00
Andy Wang e157e73ade
Check proxy method after checking admin uid to allow admin to have an invalid but unused proxy method 2022-01-26 00:58:52 +00:00
Andy Wang 67dba8c8fa
Return empty slices instead of nil for ListAllUsers 2022-01-26 00:57:18 +00:00
Andy Wang 99c4c7730a
Update Go version 2021-09-02 17:49:07 +01:00
Andy Wang 121c27fe1f
Merge pull request #168 from notsure2/fix-empty-alternative-names
Fix incorrect addition of empty name in MockDomainList if the config string contains AlternativeNames=;
2021-07-24 16:38:49 +01:00
Andy Wang d73f464358
Merge pull request #167 from notsure2/support-cloudflare
Support CloudFlare CDN. Fixes #155
2021-07-24 16:38:29 +01:00
notsure2 9d5c663618 Fix incorrect addition of empty name in MockDomainList if the config string contains AlternativeNames=; 2021-07-22 20:05:30 +02:00
notsure2 387b76d426 Support CloudFlare CDN. Fixes #155 2021-07-22 14:52:56 +02:00
Andy Wang cfdff93a1c
Update Chrome version comment (actual signature remains the same) 2021-04-30 16:00:52 +01:00
Andy Wang 8709bc0b2f
Update Firefox signature to version 88 2021-04-30 15:56:08 +01:00
Andy Wang 365f52a0e2
Add example usage of AlternativeNames config option 2021-03-26 15:59:59 +00:00
Andy Wang 88f41f755e
Add native support for Apple Silicon 2021-03-25 20:09:30 +00:00
Selim ae14e28999
ServerName rotation (#158)
* inital servername rotation

* Move MockDomainList to LocalConnConfig as the list doesn't need to be sent to the remote

* Use CSPRNG to pick the next candidate of MockDomains

Co-authored-by: Andy Wang <cbeuw.andy@gmail.com>
2021-03-25 20:06:16 +00:00
Andy Wang 91106f3c49
Prevent terminal msg from being overwritten to later concurrent writes 2021-01-12 20:59:30 +00:00
Andy Wang 32494b8d86
Prevent recvBufferSizeLimit from overflowing signed 32-bit integer 2021-01-09 00:15:46 +00:00
Andy Wang f27889af11
Allow partial json to be POSTed to admin/user/{UID} for only updating select fields 2021-01-05 21:48:02 +00:00
Andy Wang a643402e11
Remove redundant lazy allocation 2020-12-31 23:53:22 +00:00
Andy Wang 2bcb4a406d
Fix RecvDataFromRemote benchmark 2020-12-31 12:28:40 +00:00
Andy Wang 603b614009
Control flow optimisation in picking switchboard strategy 2020-12-29 23:58:18 +00:00
Andy Wang b4d65d8a0e
Use sync.Map for lock free pickRandConn 2020-12-29 19:53:14 +00:00
Andy Wang 8ab0c2d96b
Redo the implementation of switchboard and remove the need for connId 2020-12-29 14:24:54 +00:00
Andy Wang 4a2eac51fe
Cleanup some tests using testify/assert 2020-12-29 01:24:32 +00:00
Andy Wang 8dd4ae4bef
Better conform message data length to TLS standards 2020-12-29 00:16:24 +00:00
Andy Wang 439b7f0eb3
Improve encapsulation 2020-12-28 12:15:01 +00:00
Andy Wang 65443da872
Merge branch 'master' into obfs-fix 2020-12-28 12:09:44 +00:00
Andy Wang 2c709f92df
Correctly assign payloadCipher to Obfuscator field, and add test for this issue 2020-12-28 12:04:32 +00:00
77 changed files with 1504 additions and 1570 deletions

View File

@ -7,11 +7,85 @@ jobs:
matrix: matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ] os: [ ubuntu-latest, macos-latest, windows-latest ]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-go@v2 - uses: actions/setup-go@v5
with: with:
go-version: '^1.15' # 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

View File

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

2
.gitignore vendored
View File

@ -2,3 +2,5 @@ corpus/
suppressions/ suppressions/
crashers/ crashers/
*.zip *.zip
.idea/
build/

5
Dockerfile Normal file
View File

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

View File

@ -3,25 +3,30 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/cbeuw/Cloak)](https://goreportcard.com/report/github.com/cbeuw/Cloak) [![Go Report Card](https://goreportcard.com/badge/github.com/cbeuw/Cloak)](https://goreportcard.com/report/github.com/cbeuw/Cloak)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SAUYKGSREP8GL&source=url) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SAUYKGSREP8GL&source=url)
![logo](https://user-images.githubusercontent.com/7034308/96387206-3e214100-1198-11eb-8917-689d7c56e0cd.png) <p align="center">
<img src="https://user-images.githubusercontent.com/7034308/96387206-3e214100-1198-11eb-8917-689d7c56e0cd.png" />
<img src="https://user-images.githubusercontent.com/7034308/155593583-f22bcfe2-ac22-4afb-9288-1a0e8a791a0d.svg" />
</p>
![diagram](https://user-images.githubusercontent.com/7034308/65385852-7eab5280-dd2b-11e9-8887-db449b250e2a.png) <p align="center">
<img src="https://user-images.githubusercontent.com/7034308/155629720-54dd8758-ec98-4fed-b603-623f0ad83b6c.svg" />
</p>
Cloak is a [pluggable transport](https://www.ietf.org/proceedings/103/slides/slides-103-pearg-pt-slides-01) that works Cloak is a [pluggable transport](https://datatracker.ietf.org/meeting/103/materials/slides-103-pearg-pt-slides-01) that enhances
alongside traditional proxy tools like OpenVPN to evade deep-packet-inspection based censorship. 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 proxy tool's traffic as normal web browsing Cloak is not a standalone proxy program. Rather, it works by masquerading proxied traffic as normal web browsing
traffic. In contrast to traditional tools which have very prominent traffic "fingerprints", it's very difficult to activities. In contrast to traditional tools which have very prominent traffic fingerprints and can be blocked by simple filtering rules,
precisely target Cloak with little false positives. This increases the collateral damage to censorship actions as it's very difficult to precisely target Cloak with little false positives. This increases the collateral damage to censorship actions as
attempts to block Cloak could also damage services the censor state relies on. attempts to block Cloak could also damage services the censor state relies on.
To a third party observer, a host running Cloak server is indistinguishable from an innocent web server. Both while To any third party observer, a host running Cloak server is indistinguishable from an innocent web server. Both while
passively observing traffic flow to and from the server, as well as while actively probing the behaviours of a Cloak passively observing traffic flow to and from the server, as well as while actively probing the behaviours of a Cloak
server. This is achieved through the use a series server. This is achieved through the use a series
of [cryptographic stegnatography techniques](https://github.com/cbeuw/Cloak/wiki/Steganography-and-encryption). of [cryptographic steganography techniques](https://github.com/cbeuw/Cloak/wiki/Steganography-and-encryption).
Since Cloak is transparent, it can be used in conjunction with any proxy software that tunnels traffic through TCP or Cloak can be used in conjunction with any proxy program that tunnels traffic through TCP or
UDP, such as Shadowsocks, OpenVPN and Tor. Multiple proxy servers can be running on the same server host machine and UDP, such as Shadowsocks, OpenVPN and Tor. Multiple proxy servers can be running on the same server host and
Cloak server will act as a reverse proxy, bridging clients with their desired proxy end. Cloak server will act as a reverse proxy, bridging clients with their desired proxy end.
Cloak multiplexes traffic through multiple underlying TCP connections which reduces head-of-line blocking and eliminates Cloak multiplexes traffic through multiple underlying TCP connections which reduces head-of-line blocking and eliminates
@ -31,9 +36,8 @@ Cloak provides multi-user support, allowing multiple clients to connect to the p
default). It also provides traffic management features such as usage credit and bandwidth control. This allows a proxy default). It also provides traffic management features such as usage credit and bandwidth control. This allows a proxy
server to serve multiple users even if the underlying proxy software wasn't designed for multiple users server to serve multiple users even if the underlying proxy software wasn't designed for multiple users
Cloak has two modes of [_Transport_](https://github.com/cbeuw/Cloak/wiki/CDN-mode): `direct` and `CDN`. Clients can Cloak also supports tunneling through an intermediary CDN server such as Amazon Cloudfront. Such services are so widely used,
either connect to the host running Cloak server directly, or it can instead connect to a CDN edge server, which may be attempts to disrupt traffic to them can lead to very high collateral damage for the censor.
used by many other websites as well, thus further increases the collateral damage to censorship.
## Quick Start ## Quick Start
@ -133,13 +137,31 @@ 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
connection. **This may conflict with `CDN` Transport mode** if the CDN provider prohibits domain fronting and rejects
the alternative domains.
Example:
```json
{
"ServerName": "bing.com",
"AlternativeNames": ["cloudflare.com", "github.com"]
}
```
`CDNOriginHost` is the domain name of the _origin_ server (i.e. the server running Cloak) under `CDN` mode. This only `CDNOriginHost` is the domain name of the _origin_ server (i.e. the server running Cloak) under `CDN` mode. This only
has effect when `Transport` is set to `CDN`. If unset, it will default to the remote hostname supplied via the has effect when `Transport` is set to `CDN`. If unset, it will default to the remote hostname supplied via the
commandline argument (in standalone mode), or by Shadowsocks (in plugin mode). After a TLS session is established with commandline argument (in standalone mode), or by Shadowsocks (in plugin mode). After a TLS session is established with
the CDN server, this domain name will be used in the HTTP request to ask the CDN server to establish a WebSocket the CDN server, this domain name will be used in the `Host` header of the HTTP request to ask the CDN server to
connection with this host. establish a WebSocket connection with this host.
`CDNWsUrlPath` is the url path used to build websocket request sent under `CDN` mode, and also only has effect
when `Transport` is set to `CDN`. If unset, it will default to "/". This option is used to build the first line of the
HTTP request after a TLS session is extablished. It's mainly for a Cloak server behind a reverse proxy, while only
requests under specific url path are forwarded.
`NumConn` is the amount of underlying TCP connections you want to use. The default of 4 should be appropriate for most `NumConn` is the amount of underlying TCP connections you want to use. The default of 4 should be appropriate for most
people. Setting it too high will hinder the performance. Setting it to 0 will disable connection multiplexing and each people. Setting it too high will hinder the performance. Setting it to 0 will disable connection multiplexing and each
@ -147,7 +169,7 @@ TCP connection will spawn a separate short-lived session that will be closed aft
behave like GoQuiet. This maybe useful for people with unstable connections. behave like GoQuiet. This maybe useful for people with unstable connections.
`BrowserSig` is the browser you want to **appear** to be using. It's not relevant to the browser you are actually using. `BrowserSig` is the browser you want to **appear** to be using. It's not relevant to the browser you are actually using.
Currently, `chrome` and `firefox` are supported. Currently, `chrome`, `firefox` and `safari` are supported.
`KeepAlive` is the number of seconds to tell the OS to wait after no activity before sending TCP KeepAlive probes to the `KeepAlive` is the number of seconds to tell the OS to wait after no activity before sending TCP KeepAlive probes to the
Cloak server. Zero or negative value disables it. Default is 0 (disabled). Warning: Enabling it might make your server Cloak server. Zero or negative value disables it. Default is 0 (disabled). Warning: Enabling it might make your server

View File

@ -1,3 +1,4 @@
//go:build go1.11
// +build go1.11 // +build go1.11
package main package main
@ -7,10 +8,11 @@ import (
"encoding/binary" "encoding/binary"
"flag" "flag"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"net" "net"
"os" "os"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/client" "github.com/cbeuw/Cloak/internal/client"
mux "github.com/cbeuw/Cloak/internal/multiplex" mux "github.com/cbeuw/Cloak/internal/multiplex"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -173,6 +175,11 @@ func main() {
log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod) log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod)
seshMaker = func() *mux.Session { seshMaker = func() *mux.Session {
authInfo := authInfo // copy the struct because we are overwriting SessionId authInfo := authInfo // copy the struct because we are overwriting SessionId
randByte := make([]byte, 1)
common.RandRead(authInfo.WorldState.Rand, randByte)
authInfo.MockDomain = localConfig.MockDomainList[int(randByte[0])%len(localConfig.MockDomainList)]
// sessionID is usergenerated. There shouldn't be a security concern because the scope of // sessionID is usergenerated. There shouldn't be a security concern because the scope of
// sessionID is limited to its UID. // sessionID is limited to its UID.
quad := make([]byte, 4) quad := make([]byte, 4)

View File

@ -1,3 +1,4 @@
//go:build !android
// +build !android // +build !android
package main package main

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build android
// +build android // +build android
package main package main
@ -28,9 +29,10 @@ import "C"
import ( import (
"bufio" "bufio"
log "github.com/sirupsen/logrus"
"os" "os"
"unsafe" "unsafe"
log "github.com/sirupsen/logrus"
) )
var ( var (

View File

@ -1,3 +1,4 @@
//go:build !android
// +build !android // +build !android
package main package main

View File

@ -1,4 +1,6 @@
//go:build android
// +build android // +build android
package main package main
// Stolen from https://github.com/shadowsocks/overture/blob/shadowsocks/core/utils/utils_android.go // Stolen from https://github.com/shadowsocks/overture/blob/shadowsocks/core/utils/utils_android.go
@ -64,8 +66,9 @@ void set_timeout(int sock) {
import "C" import "C"
import ( import (
log "github.com/sirupsen/logrus"
"syscall" "syscall"
log "github.com/sirupsen/logrus"
) )
// In Android, once an app starts the VpnService, all outgoing traffic are routed by the system // In Android, once an app starts the VpnService, all outgoing traffic are routed by the system

View File

@ -3,15 +3,16 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server"
log "github.com/sirupsen/logrus"
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"runtime" "runtime"
"strings" "strings"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server"
log "github.com/sirupsen/logrus"
) )
var version string var version string

View File

@ -1,57 +1,36 @@
package main package main
import ( import (
"github.com/stretchr/testify/assert"
"net" "net"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestParseBindAddr(t *testing.T) { func TestParseBindAddr(t *testing.T) {
t.Run("port only", func(t *testing.T) { t.Run("port only", func(t *testing.T) {
addrs, err := resolveBindAddr([]string{":443"}) addrs, err := resolveBindAddr([]string{":443"})
if err != nil { assert.NoError(t, err)
t.Error(err) assert.Equal(t, ":443", addrs[0].String())
return
}
if addrs[0].String() != ":443" {
t.Errorf("expected %v got %v", ":443", addrs[0].String())
}
}) })
t.Run("specific address", func(t *testing.T) { t.Run("specific address", func(t *testing.T) {
addrs, err := resolveBindAddr([]string{"192.168.1.123:443"}) addrs, err := resolveBindAddr([]string{"192.168.1.123:443"})
if err != nil { assert.NoError(t, err)
t.Error(err) assert.Equal(t, "192.168.1.123:443", addrs[0].String())
return
}
if addrs[0].String() != "192.168.1.123:443" {
t.Errorf("expected %v got %v", "192.168.1.123:443", addrs[0].String())
}
}) })
t.Run("ipv6", func(t *testing.T) { t.Run("ipv6", func(t *testing.T) {
addrs, err := resolveBindAddr([]string{"[::]:443"}) addrs, err := resolveBindAddr([]string{"[::]:443"})
if err != nil { assert.NoError(t, err)
t.Error(err) assert.Equal(t, "[::]:443", addrs[0].String())
return
}
if addrs[0].String() != "[::]:443" {
t.Errorf("expected %v got %v", "[::]:443", addrs[0].String())
}
}) })
t.Run("mixed", func(t *testing.T) { t.Run("mixed", func(t *testing.T) {
addrs, err := resolveBindAddr([]string{":80", "[::]:443"}) addrs, err := resolveBindAddr([]string{":80", "[::]:443"})
if err != nil { assert.NoError(t, err)
t.Error(err) assert.Equal(t, ":80", addrs[0].String())
return assert.Equal(t, "[::]:443", addrs[1].String())
}
if addrs[0].String() != ":80" {
t.Errorf("expected %v got %v", ":80", addrs[0].String())
}
if addrs[1].String() != "[::]:443" {
t.Errorf("expected %v got %v", "[::]:443", addrs[1].String())
}
}) })
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"github.com/cbeuw/Cloak/internal/common" "github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh" "github.com/cbeuw/Cloak/internal/ecdh"
) )

View File

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

44
go.mod
View File

@ -1,24 +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-20200411160121-c5a5c4a9de14 github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
github.com/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b // indirect github.com/gorilla/mux v1.8.1
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect github.com/gorilla/websocket v1.5.3
github.com/gorilla/mux v1.7.3 github.com/juju/ratelimit v1.0.2
github.com/gorilla/websocket v1.4.1 github.com/refraction-networking/utls v1.8.0
github.com/juju/ratelimit v1.0.1 github.com/sirupsen/logrus v1.9.3
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/stretchr/testify v1.10.0
github.com/kr/pretty v0.1.0 // indirect go.etcd.io/bbolt v1.4.0
github.com/mitchellh/gox v1.0.1 // indirect golang.org/x/crypto v0.37.0
github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 )
github.com/sirupsen/logrus v1.5.0
github.com/stephens2424/writerset v1.0.2 // indirect require (
github.com/stretchr/testify v1.6.1 github.com/andybalholm/brotli v1.1.1 // indirect
go.etcd.io/bbolt v1.3.4 github.com/cloudflare/circl v1.6.1 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 github.com/davecgh/go-spew v1.1.1 // indirect
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect github.com/klauspost/compress v1.18.0 // indirect
golang.org/x/tools v0.0.0-20201015182029-a5d9e455e9c4 // indirect github.com/kr/pretty v0.3.1 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // 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
) )

126
go.sum
View File

@ -1,85 +1,61 @@
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/cbeuw/connutil v0.0.0-20200411160121-c5a5c4a9de14 h1:bWJKlzTJR7C9DX0l1qhkTaP1lTEBWVDKhg8C/tNJqKg= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cbeuw/connutil v0.0.0-20200411160121-c5a5c4a9de14/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE= 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/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/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b h1:CXfDl9Y3NKuhOSxF9kXhiLmuYCdufQDrLY2fO1BzqBU= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
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/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 h1:SL1K0QAuC1b54KoY1pjPWe6kSlsFHwK9/oC960fKrTY= github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0= github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= github.com/refraction-networking/utls v1.7.0/go.mod h1:lV0Gwc1/Fi+HYH8hOtgFRdHfKo4FKSn6+FdyOz9hRms=
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8= github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= 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 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201015182029-a5d9e455e9c4 h1:rQWkJiVIyJ3PgiSHL+RXc8xbrK8duU6jG5eeZ9G7nk8=
golang.org/x/tools v0.0.0-20201015182029-a5d9e455e9c4/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=

View File

@ -1,10 +1,11 @@
package client package client
import ( import (
"encoding/binary"
"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" "net"
"strings"
) )
const appDataMaxLength = 16401 const appDataMaxLength = 16401
@ -13,59 +14,125 @@ type clientHelloFields struct {
random []byte random []byte
sessionId []byte sessionId []byte
x25519KeyShare []byte x25519KeyShare []byte
sni []byte serverName string
} }
type browser interface { type browser int
composeClientHello(clientHelloFields) []byte
}
func makeServerName(serverName string) []byte { const (
serverNameListLength := make([]byte, 2) chrome = iota
binary.BigEndian.PutUint16(serverNameListLength, uint16(len(serverName)+3)) firefox
serverNameType := []byte{0x00} // host_name safari
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
}
func genStegClientHello(ai authenticationPayload, serverName string) (ret clientHelloFields) {
// random is marshalled ephemeral pub key 32 bytes
// The authentication ciphertext and its tag are then distributed among SessionId and X25519KeyShare
ret.random = ai.randPubKey[:]
ret.sessionId = ai.ciphertextWithTag[0:32]
ret.x25519KeyShare = ai.ciphertextWithTag[32:64]
ret.sni = makeServerName(serverName)
return
}
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)
chOnly := tls.browser.composeClientHello(genStegClientHello(payload, authInfo.MockDomain))
chWithRecordLayer := common.AddRecordLayer(chOnly, common.Handshake, common.VersionTLS11) fields := clientHelloFields{
random: payload.randPubKey[:],
sessionId: payload.ciphertextWithTag[0:32],
x25519KeyShare: payload.ciphertextWithTag[32:64],
serverName: authInfo.MockDomain,
}
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

View File

@ -1,43 +0,0 @@
package client
import (
"bytes"
"encoding/hex"
"testing"
)
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 {
if !bytes.Equal(makeServerName(p.serverName), p.target) {
t.Error(
"for", p.serverName,
"expecting", p.target,
"got", makeServerName(p.serverName))
}
}
}

View File

@ -2,6 +2,7 @@ package client
import ( import (
"encoding/binary" "encoding/binary"
"github.com/cbeuw/Cloak/internal/common" "github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh" "github.com/cbeuw/Cloak/internal/ecdh"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"

View File

@ -2,10 +2,12 @@ package client
import ( import (
"bytes" "bytes"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/multiplex"
"testing" "testing"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/multiplex"
"github.com/stretchr/testify/assert"
) )
func TestMakeAuthenticationPayload(t *testing.T) { func TestMakeAuthenticationPayload(t *testing.T) {
@ -64,12 +66,8 @@ func TestMakeAuthenticationPayload(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
func() { func() {
payload, sharedSecret := makeAuthenticationPayload(tc.authInfo) payload, sharedSecret := makeAuthenticationPayload(tc.authInfo)
if payload != tc.expPayload { assert.Equal(t, tc.expPayload, payload, "payload doesn't match")
t.Errorf("payload doesn't match:\nexp %v\ngot %v", tc.expPayload, payload) assert.Equal(t, tc.expSecret, sharedSecret, "shared secret doesn't match")
}
if sharedSecret != tc.expSecret {
t.Errorf("secret doesn't match:\nexp %x\ngot %x", tc.expPayload, payload)
}
}() }()
} }
} }

View File

@ -1,103 +0,0 @@
// Fingerprint of Chrome 85
package client
import (
"encoding/binary"
"encoding/hex"
"github.com/cbeuw/Cloak/internal/common"
)
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(sni []byte, 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
}
// extension length is always 403, and server name length is variable
var ext [17][]byte
ext[0] = addExtRec(makeGREASE(), nil) // First GREASE
ext[1] = addExtRec([]byte{0x00, 0x00}, sni) // 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, 0x23}, nil) // Session tickets
APLN, _ := hex.DecodeString("000c02683208687474702f312e31")
ext[7] = addExtRec([]byte{0x00, 0x10}, APLN) // app layer proto negotiation
ext[8] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
sigAlgo, _ := hex.DecodeString("001004030804040105030805050108060601")
ext[9] = addExtRec([]byte{0x00, 0x0d}, sigAlgo) // 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, _ := hex.DecodeString("0a9A9A0304030303020301") // 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(makeGREASE(), []byte{0x00}) // Last GREASE
// len(ext[1]) + 170 + len(ext[16]) = 403
// len(ext[16]) = 233 - len(ext[1])
// 2+2+len(padding) = 233 - len(ext[1])
// len(padding) = 229 - len(ext[1])
ext[16] = addExtRec([]byte{0x00, 0x15}, make([]byte, 229-len(ext[1]))) // 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 34
cipherSuites, _ := hex.DecodeString("130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035")
clientHello[7] = append(makeGREASE(), cipherSuites...) // cipher suites
clientHello[8] = []byte{0x01} // compression methods length 1
clientHello[9] = []byte{0x00} // compression methods
clientHello[11] = c.composeExtensions(hd.sni, hd.x25519KeyShare)
clientHello[10] = []byte{0x00, 0x00} // extensions length 403
binary.BigEndian.PutUint16(clientHello[10], uint16(len(clientHello[11])))
var ret []byte
for _, c := range clientHello {
ret = append(ret, c...)
}
return ret
}

View File

@ -1,48 +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 TestComposeExtension(t *testing.T) {
serverName := "github.com"
keyShare, _ := hex.DecodeString("690f074f5c01756982269b66d58c90c47dc0f281d654c7b2c16f63c9033f5604")
sni := makeServerName(serverName)
result := (&Chrome{}).composeExtensions(sni, keyShare)
target, _ := hex.DecodeString("8a8a00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000a00088a8a001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b00298a8a000100001d0020690f074f5c01756982269b66d58c90c47dc0f281d654c7b2c16f63c9033f5604002d00020101002b000b0a3a3a0304030303020301001b00030200024a4a000100001500d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
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)
}
}
}

View File

@ -1,12 +1,13 @@
package client package client
import ( import (
"github.com/cbeuw/Cloak/internal/common"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex" mux "github.com/cbeuw/Cloak/internal/multiplex"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -20,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)
@ -30,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

View File

@ -1,77 +0,0 @@
// Fingerprint of Firefox 68
package client
import (
"encoding/binary"
"encoding/hex"
"github.com/cbeuw/Cloak/internal/common"
)
type Firefox struct{}
func (f *Firefox) composeExtensions(SNI []byte, 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 399, and server name length is variable
var ext [14][]byte
ext[0] = addExtRec([]byte{0x00, 0x00}, SNI) // server name indication
ext[1] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret
ext[2] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info
suppGroup, _ := hex.DecodeString("000c001d00170018001901000101")
ext[3] = addExtRec([]byte{0x00, 0x0a}, suppGroup) // supported groups
ext[4] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats
ext[5] = addExtRec([]byte{0x00, 0x23}, []byte{}) // Session tickets
APLN, _ := hex.DecodeString("000c02683208687474702f312e31")
ext[6] = addExtRec([]byte{0x00, 0x10}, APLN) // app layer proto negotiation
ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
ext[8] = addExtRec([]byte{0x00, 0x33}, composeKeyShare(keyShare)) // key share
suppVersions, _ := hex.DecodeString("080304030303020301")
ext[9] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
sigAlgo, _ := hex.DecodeString("001604030503060308040805080604010501060102030201")
ext[10] = addExtRec([]byte{0x00, 0x0d}, sigAlgo) // Signature Algorithms
ext[11] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
ext[12] = addExtRec([]byte{0x00, 0x1c}, []byte{0x40, 0x01}) // record size limit
// len(ext[0]) + 237 + 4 + len(padding) = 399
// len(padding) = 158 - len(ext[0])
ext[13] = addExtRec([]byte{0x00, 0x15}, make([]byte, 163-len(SNI))) // 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, 0x24} // cipher suites length 36
cipherSuites, _ := hex.DecodeString("130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a")
clientHello[7] = cipherSuites // cipher suites
clientHello[8] = []byte{0x01} // compression methods length 1
clientHello[9] = []byte{0x00} // compression methods
clientHello[11] = f.composeExtensions(hd.sni, hd.x25519KeyShare)
clientHello[10] = []byte{0x00, 0x00} // extensions length
binary.BigEndian.PutUint16(clientHello[10], uint16(len(clientHello[11])))
var ret []byte
for _, c := range clientHello {
ret = append(ret, c...)
}
return ret
}

View File

@ -1,20 +0,0 @@
package client
import (
"bytes"
"encoding/hex"
"testing"
)
func TestComposeExtensions(t *testing.T) {
target, _ := hex.DecodeString("000000170015000012636f6e73656e742e676f6f676c652e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00206075db0a43812b2e4e0f44157f04295b484ccfc6d70e577c1e6113aa18e088270017004104948052ae52043e654641660ebbadb527c8280262e61f64b0f6f1794f32e1000865a49e4cbe2027c78e7180861e4336300815fa0f1b0091c4d788b97f809a47d3002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015008c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
serverName := "consent.google.com"
keyShare, _ := hex.DecodeString("6075db0a43812b2e4e0f44157f04295b484ccfc6d70e577c1e6113aa18e08827")
sni := makeServerName(serverName)
result := (&Firefox{}).composeExtensions(sni, keyShare)
// skip random secp256r1
if !bytes.Equal(result[:137], target[:137]) || !bytes.Equal(result[202:], target[202:]) {
t.Errorf("got %x", result)
}
}

View File

@ -1,12 +1,13 @@
package client package client
import ( import (
"github.com/cbeuw/Cloak/internal/common"
"io" "io"
"net" "net"
"sync" "sync"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex" mux "github.com/cbeuw/Cloak/internal/multiplex"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -4,13 +4,14 @@ import (
"crypto" "crypto"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus"
"io/ioutil" "io/ioutil"
"net" "net"
"strings" "strings"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus"
"github.com/cbeuw/Cloak/internal/ecdh" "github.com/cbeuw/Cloak/internal/ecdh"
mux "github.com/cbeuw/Cloak/internal/multiplex" mux "github.com/cbeuw/Cloak/internal/multiplex"
) )
@ -30,12 +31,13 @@ type RawConfig struct {
LocalPort string // jsonOptional LocalPort string // jsonOptional
RemoteHost string // jsonOptional RemoteHost string // jsonOptional
RemotePort string // jsonOptional RemotePort string // jsonOptional
AlternativeNames []string // jsonOptional
// defaults set in ProcessRawConfig // defaults set in ProcessRawConfig
UDP bool // nullable UDP bool // nullable
BrowserSig string // nullable BrowserSig string // nullable
Transport string // nullable Transport string // nullable
CDNOriginHost string // nullable CDNOriginHost string // nullable
CDNWsUrlPath string // nullable
StreamTimeout int // nullable StreamTimeout int // nullable
KeepAlive int // nullable KeepAlive int // nullable
} }
@ -45,12 +47,13 @@ type RemoteConnConfig struct {
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 {
LocalAddr string LocalAddr string
Timeout time.Duration Timeout time.Duration
MockDomainList []string
} }
type AuthInfo struct { type AuthInfo struct {
@ -94,6 +97,20 @@ func ssvToJson(ssv string) (ret []byte) {
} }
key := sp[0] key := sp[0]
value := sp[1] value := sp[1]
if strings.HasPrefix(key, "AlternativeNames") {
switch strings.Contains(value, ",") {
case true:
domains := strings.Split(value, ",")
for index, domain := range domains {
domains[index] = `"` + domain + `"`
}
value = strings.Join(domains, ",")
ret = append(ret, []byte(`"`+key+`":[`+value+`],`)...)
case false:
ret = append(ret, []byte(`"`+key+`":["`+value+`"],`)...)
}
continue
}
// JSON doesn't like quotation marks around int and bool // JSON doesn't like quotation marks around int and bool
// This is extremely ugly but it's still better than writing a tokeniser // This is extremely ugly but it's still better than writing a tokeniser
if elem(key, unquoted) { if elem(key, unquoted) {
@ -139,6 +156,17 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
return nullErr("ServerName") return nullErr("ServerName")
} }
auth.MockDomain = raw.ServerName auth.MockDomain = raw.ServerName
var filteredAlternativeNames []string
for _, alternativeName := range raw.AlternativeNames {
if len(alternativeName) > 0 {
filteredAlternativeNames = append(filteredAlternativeNames, alternativeName)
}
}
raw.AlternativeNames = filteredAlternativeNames
local.MockDomainList = raw.AlternativeNames
local.MockDomainList = append(local.MockDomainList, auth.MockDomain)
if raw.ProxyMethod == "" { if raw.ProxyMethod == "" {
return nullErr("ServerName") return nullErr("ServerName")
} }
@ -198,11 +226,13 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
} else { } else {
cdnDomainPort = net.JoinHostPort(raw.CDNOriginHost, raw.RemotePort) cdnDomainPort = net.JoinHostPort(raw.CDNOriginHost, raw.RemotePort)
} }
if raw.CDNWsUrlPath == "" {
remote.TransportMaker = func() Transport { raw.CDNWsUrlPath = "/"
return &WSOverTLS{
cdnDomainPort: cdnDomainPort,
} }
remote.Transport = TransportConfig{
mode: "cdn",
wsUrl: "ws://" + cdnDomainPort + raw.CDNWsUrlPath,
} }
case "direct": case "direct":
fallthrough fallthrough
@ -210,18 +240,19 @@ 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":
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,
} }
} }
}
// KeepAlive // KeepAlive
if raw.KeepAlive <= 0 { if raw.KeepAlive <= 0 {

View File

@ -1,9 +1,10 @@
package client package client
import ( import (
"github.com/stretchr/testify/assert"
"io/ioutil" "io/ioutil"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {

View File

@ -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
}
}

View File

@ -4,17 +4,18 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/gorilla/websocket"
utls "github.com/refraction-networking/utls"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"github.com/cbeuw/Cloak/internal/common"
"github.com/gorilla/websocket"
utls "github.com/refraction-networking/utls"
) )
type WSOverTLS struct { type WSOverTLS struct {
*common.WebSocketConn *common.WebSocketConn
cdnDomainPort string wsUrl string
} }
func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) { func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
@ -23,12 +24,24 @@ func (ws *WSOverTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey
InsecureSkipVerify: true, InsecureSkipVerify: true,
} }
uconn := utls.UClient(rawConn, utlsConfig, utls.HelloChrome_Auto) uconn := utls.UClient(rawConn, utlsConfig, utls.HelloChrome_Auto)
err = uconn.BuildHandshakeState()
if err != nil {
return
}
for i, extension := range uconn.Extensions {
_, ok := extension.(*utls.ALPNExtension)
if ok {
uconn.Extensions = append(uconn.Extensions[:i], uconn.Extensions[i+1:]...)
break
}
}
err = uconn.Handshake() err = uconn.Handshake()
if err != nil { if err != nil {
return return
} }
u, err := url.Parse("ws://" + ws.cdnDomainPort) u, err := url.Parse(ws.wsUrl)
if err != nil { if err != nil {
return sessionKey, fmt.Errorf("failed to parse ws url: %v", err) return sessionKey, fmt.Errorf("failed to parse ws url: %v", err)
} }

View File

@ -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
} }

View File

@ -4,10 +4,11 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors" "errors"
"github.com/stretchr/testify/assert"
"io" "io"
"math/rand" "math/rand"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
const gcmTagSize = 16 const gcmTagSize = 16

View File

@ -2,6 +2,7 @@ package common
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"io" "io"
"net" "net"
"sync" "sync"
@ -94,6 +95,9 @@ func (tls *TLSConn) Read(buffer []byte) (n int, err error) {
func (tls *TLSConn) Write(in []byte) (n int, err error) { func (tls *TLSConn) Write(in []byte) (n int, err error) {
msgLen := len(in) msgLen := len(in)
if msgLen > 1<<14+256 { // https://tools.ietf.org/html/rfc8446#section-5.2
return 0, errors.New("message is too long")
}
writeBuf := tls.writeBufPool.Get().(*[]byte) writeBuf := tls.writeBufPool.Get().(*[]byte)
*writeBuf = append(*writeBuf, byte(msgLen>>8), byte(msgLen&0xFF)) *writeBuf = append(*writeBuf, byte(msgLen>>8), byte(msgLen&0xFF))
*writeBuf = append(*writeBuf, in...) *writeBuf = append(*writeBuf, in...)

View File

@ -2,10 +2,11 @@ package common
import ( import (
"errors" "errors"
"github.com/gorilla/websocket"
"io" "io"
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket"
) )
// WebSocketConn implements io.ReadWriteCloser // WebSocketConn implements io.ReadWriteCloser

View File

@ -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()

View File

@ -1,9 +1,10 @@
package multiplex package multiplex
import ( import (
"bytes"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
func TestDatagramBuffer_RW(t *testing.T) { func TestDatagramBuffer_RW(t *testing.T) {
@ -11,13 +12,7 @@ func TestDatagramBuffer_RW(t *testing.T) {
t.Run("simple write", func(t *testing.T) { t.Run("simple write", func(t *testing.T) {
pipe := NewDatagramBufferedPipe() pipe := NewDatagramBufferedPipe()
_, err := pipe.Write(&Frame{Payload: b}) _, err := pipe.Write(&Frame{Payload: b})
if err != nil { assert.NoError(t, err)
t.Error(
"expecting", "nil error",
"got", err,
)
return
}
}) })
t.Run("simple read", func(t *testing.T) { t.Run("simple read", func(t *testing.T) {
@ -25,50 +20,18 @@ func TestDatagramBuffer_RW(t *testing.T) {
_, _ = pipe.Write(&Frame{Payload: b}) _, _ = pipe.Write(&Frame{Payload: b})
b2 := make([]byte, len(b)) b2 := make([]byte, len(b))
n, err := pipe.Read(b2) n, err := pipe.Read(b2)
if n != len(b) { assert.NoError(t, err)
t.Error( assert.Equal(t, len(b), n)
"For", "number of bytes read", assert.Equal(t, b, b2)
"expecting", len(b), assert.Equal(t, 0, pipe.buf.Len(), "buf len is not 0 after finished reading")
"got", n,
)
return
}
if err != nil {
t.Error(
"expecting", "nil error",
"got", err,
)
return
}
if !bytes.Equal(b, b2) {
t.Error(
"expecting", b,
"got", b2,
)
}
if pipe.buf.Len() != 0 {
t.Error("buf len is not 0 after finished reading")
return
}
}) })
t.Run("writing closing frame", func(t *testing.T) { t.Run("writing closing frame", func(t *testing.T) {
pipe := NewDatagramBufferedPipe() pipe := NewDatagramBufferedPipe()
toBeClosed, err := pipe.Write(&Frame{Closing: closingStream}) toBeClosed, err := pipe.Write(&Frame{Closing: closingStream})
if !toBeClosed { assert.NoError(t, err)
t.Error("should be to be closed") assert.True(t, toBeClosed, "should be to be closed")
} assert.True(t, pipe.closed, "pipe should be closed")
if err != nil {
t.Error(
"expecting", "nil error",
"got", err,
)
return
}
if !pipe.closed {
t.Error("expecting closed pipe, not closed")
}
}) })
} }
@ -81,30 +44,9 @@ func TestDatagramBuffer_BlockingRead(t *testing.T) {
}() }()
b2 := make([]byte, len(b)) b2 := make([]byte, len(b))
n, err := pipe.Read(b2) n, err := pipe.Read(b2)
if n != len(b) { assert.NoError(t, err)
t.Error( assert.Equal(t, len(b), n, "number of bytes read after block is wrong")
"For", "number of bytes read after block", assert.Equal(t, b, b2)
"expecting", len(b),
"got", n,
)
return
}
if err != nil {
t.Error(
"For", "blocked read",
"expecting", "nil error",
"got", err,
)
return
}
if !bytes.Equal(b, b2) {
t.Error(
"For", "blocked read",
"expecting", b,
"got", b2,
)
return
}
} }
func TestDatagramBuffer_CloseThenRead(t *testing.T) { func TestDatagramBuffer_CloseThenRead(t *testing.T) {
@ -114,27 +56,7 @@ func TestDatagramBuffer_CloseThenRead(t *testing.T) {
b2 := make([]byte, len(b)) b2 := make([]byte, len(b))
pipe.Close() pipe.Close()
n, err := pipe.Read(b2) n, err := pipe.Read(b2)
if n != len(b) { assert.NoError(t, err)
t.Error( assert.Equal(t, len(b), n, "number of bytes read after block is wrong")
"For", "number of bytes read", assert.Equal(t, b, b2)
"expecting", len(b),
"got", n,
)
}
if err != nil {
t.Error(
"For", "simple read",
"expecting", "nil error",
"got", err,
)
return
}
if !bytes.Equal(b, b2) {
t.Error(
"For", "simple read",
"expecting", b,
"got", b2,
)
return
}
} }

View File

@ -2,14 +2,15 @@ package multiplex
import ( import (
"bytes" "bytes"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
"io" "io"
"math/rand" "math/rand"
"net" "net"
"sync" "sync"
"testing" "testing"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
) )
func serveEcho(l net.Listener) { func serveEcho(l net.Listener) {
@ -108,9 +109,7 @@ func TestMultiplex(t *testing.T) {
streams := make([]net.Conn, numStreams) streams := make([]net.Conn, numStreams)
for i := 0; i < numStreams; i++ { for i := 0; i < numStreams; i++ {
stream, err := clientSession.OpenStream() stream, err := clientSession.OpenStream()
if err != nil { assert.NoError(t, err)
t.Fatalf("failed to open stream: %v", err)
}
streams[i] = stream streams[i] = stream
} }
@ -123,18 +122,11 @@ func TestMultiplex(t *testing.T) {
// close one stream // close one stream
closing, streams := streams[0], streams[1:] closing, streams := streams[0], streams[1:]
err := closing.Close() err := closing.Close()
if err != nil { assert.NoError(t, err, "couldn't close a stream")
t.Errorf("couldn't close a stream")
}
_, err = closing.Write([]byte{0}) _, err = closing.Write([]byte{0})
if err != ErrBrokenStream { assert.Equal(t, ErrBrokenStream, err)
t.Errorf("expecting error %v, got %v", ErrBrokenStream, err)
}
_, err = closing.Read(make([]byte, 1)) _, err = closing.Read(make([]byte, 1))
if err != ErrBrokenStream { assert.Equal(t, ErrBrokenStream, err)
t.Errorf("expecting error %v, got %v", ErrBrokenStream, err)
}
} }
func TestMux_StreamClosing(t *testing.T) { func TestMux_StreamClosing(t *testing.T) {
@ -146,20 +138,13 @@ func TestMux_StreamClosing(t *testing.T) {
recvBuf := make([]byte, 128) recvBuf := make([]byte, 128)
toBeClosed, _ := clientSession.OpenStream() toBeClosed, _ := clientSession.OpenStream()
_, err := toBeClosed.Write(testData) // should be echoed back _, err := toBeClosed.Write(testData) // should be echoed back
if err != nil { assert.NoError(t, err, "couldn't write to a stream")
t.Errorf("can't write to stream: %v", err)
}
_, err = io.ReadFull(toBeClosed, recvBuf[:1]) _, err = io.ReadFull(toBeClosed, recvBuf[:1])
if err != nil { assert.NoError(t, err, "can't read anything before stream closed")
t.Errorf("can't read anything before stream closed: %v", err)
}
_ = toBeClosed.Close() _ = toBeClosed.Close()
_, err = io.ReadFull(toBeClosed, recvBuf[1:]) _, err = io.ReadFull(toBeClosed, recvBuf[1:])
if err != nil { assert.NoError(t, err, "can't read residual data on stream")
t.Errorf("can't read residual data on stream: %v", err) assert.Equal(t, testData, recvBuf, "incorrect data read back")
}
if !bytes.Equal(testData, recvBuf) {
t.Errorf("incorrect data read back")
}
} }

View File

@ -3,6 +3,7 @@ package multiplex
import ( import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
@ -11,12 +12,17 @@ import (
"golang.org/x/crypto/salsa20" "golang.org/x/crypto/salsa20"
) )
type Obfser func(*Frame, []byte, int) (int, error)
type Deobfser func(*Frame, []byte) error
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
@ -26,25 +32,17 @@ const (
// Obfuscator is responsible for serialisation, obfuscation, and optional encryption of data frames. // Obfuscator is responsible for serialisation, obfuscation, and optional encryption of data frames.
type Obfuscator struct { type Obfuscator struct {
// Used in Stream.Write. Add multiplexing headers, encrypt and add TLS header payloadCipher cipher.AEAD
Obfs Obfser
// Remove TLS header, decrypt and unmarshall frames
Deobfs Deobfser
SessionKey [32]byte
maxOverhead int sessionKey [32]byte
} }
// MakeObfs returns a function of type Obfser. An Obfser takes three arguments: // obfuscate adds multiplexing headers, encrypt and add TLS header
// a *Frame with all the field set correctly, a []byte as buffer to put encrypted func (o *Obfuscator) obfuscate(f *Frame, buf []byte, payloadOffsetInBuf int) (int, error) {
// message in, and an int called payloadOffsetInBuf to be used when *Frame.payload
// is in the byte slice used as buffer (2nd argument). payloadOffsetInBuf specifies
// the index at which data belonging to *Frame.Payload starts in the buffer.
func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
// The method here is to use the first payloadCipher.NonceSize() bytes of the serialised frame header // The method here is to use the first payloadCipher.NonceSize() bytes of the serialised frame header
// as iv/nonce for the AEAD cipher to encrypt the frame payload. Then we use // as iv/nonce for the AEAD cipher to encrypt the frame payload. Then we use
// the authentication tag produced appended to the end of the ciphertext (of size payloadCipher.Overhead()) // the authentication tag produced appended to the end of the ciphertext (of size payloadCipher.Overhead())
// as nonce for Salsa20 to encrypt the frame header. Both with SessionKey as keys. // as nonce for Salsa20 to encrypt the frame header. Both with sessionKey as keys.
// //
// Several cryptographic guarantees we have made here: that payloadCipher, as an AEAD, is given a unique // Several cryptographic guarantees we have made here: that payloadCipher, as an AEAD, is given a unique
// iv/nonce each time, relative to its key; that the frame header encryptor Salsa20 is given a unique // iv/nonce each time, relative to its key; that the frame header encryptor Salsa20 is given a unique
@ -57,46 +55,34 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
// 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.
obfs := func(f *Frame, buf []byte, payloadOffsetInBuf int) (int, error) {
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 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 = 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)
@ -106,41 +92,35 @@ func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
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 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 {
payloadCipher.Seal(payload[:0], header[:payloadCipher.NonceSize()], payload, nil) if o.payloadCipher != nil {
o.payloadCipher.Seal(payload[:0], header[:o.payloadCipher.NonceSize()], payload, nil)
} }
nonce := buf[usefulLen-salsa20NonceSize : usefulLen] nonce := buf[usefulLen-salsa20NonceSize : usefulLen]
salsa20.XORKeyStream(header, header, nonce, &salsaKey) salsa20.XORKeyStream(header, header, nonce, &o.sessionKey)
return usefulLen, nil return usefulLen, nil
}
return obfs
} }
// MakeDeobfs returns a function Deobfser. A Deobfser takes in a single byte slice, // deobfuscate removes TLS header, decrypt and unmarshall frames
// containing the message to be decrypted, and returns a *Frame containing the frame func (o *Obfuscator) deobfuscate(f *Frame, in []byte) error {
// information and plaintext if len(in) < frameHeaderLength+salsa20NonceSize {
func MakeDeobfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Deobfser { return fmt.Errorf("input size %v, but it cannot be shorter than %v bytes", len(in), frameHeaderLength+salsa20NonceSize)
// frame header length + minimum data size (i.e. nonce size of salsa20)
const minInputLen = frameHeaderLength + salsa20NonceSize
deobfs := func(f *Frame, in []byte) error {
if len(in) < minInputLen {
return fmt.Errorf("input size %v, but it cannot be shorter than %v bytes", len(in), minInputLen)
} }
header := in[:frameHeaderLength] header := in[:frameHeaderLength]
pldWithOverHead := in[frameHeaderLength:] // payload + potential overhead pldWithOverHead := in[frameHeaderLength:] // payload + potential overhead
nonce := in[len(in)-salsa20NonceSize:] nonce := in[len(in)-salsa20NonceSize:]
salsa20.XORKeyStream(header, header, nonce, &salsaKey) salsa20.XORKeyStream(header, header, nonce, &o.sessionKey)
streamID := binary.BigEndian.Uint32(header[0:4]) streamID := binary.BigEndian.Uint32(header[0:4])
seq := binary.BigEndian.Uint64(header[4:12]) seq := binary.BigEndian.Uint64(header[4:12])
@ -154,14 +134,14 @@ func MakeDeobfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Deobfser {
var outputPayload []byte var outputPayload []byte
if payloadCipher == nil { if o.payloadCipher == nil {
if extraLen == 0 { if extraLen == 0 {
outputPayload = pldWithOverHead outputPayload = pldWithOverHead
} else { } else {
outputPayload = pldWithOverHead[:usefulPayloadLen] outputPayload = pldWithOverHead[:usefulPayloadLen]
} }
} else { } else {
_, err := payloadCipher.Open(pldWithOverHead[:0], header[:payloadCipher.NonceSize()], pldWithOverHead, nil) _, err := o.payloadCipher.Open(pldWithOverHead[:0], header[:o.payloadCipher.NonceSize()], pldWithOverHead, nil)
if err != nil { if err != nil {
return err return err
} }
@ -173,58 +153,49 @@ func MakeDeobfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Deobfser {
f.Closing = closing f.Closing = closing
f.Payload = outputPayload f.Payload = outputPayload
return nil return nil
}
return deobfs
} }
func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (obfuscator Obfuscator, err error) { func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (o Obfuscator, err error) {
obfuscator = Obfuscator{ o = Obfuscator{
SessionKey: sessionKey, sessionKey: sessionKey,
} }
var payloadCipher cipher.AEAD
switch encryptionMethod { switch encryptionMethod {
case EncryptionMethodPlain: case EncryptionMethodPlain:
payloadCipher = nil o.payloadCipher = nil
obfuscator.maxOverhead = salsa20NonceSize
case EncryptionMethodAES256GCM: case EncryptionMethodAES256GCM:
var c cipher.Block var c cipher.Block
c, err = aes.NewCipher(sessionKey[:]) c, err = aes.NewCipher(sessionKey[:])
if err != nil { if err != nil {
return return
} }
payloadCipher, err = cipher.NewGCM(c) o.payloadCipher, err = cipher.NewGCM(c)
if err != nil { if err != nil {
return return
} }
obfuscator.maxOverhead = 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])
if err != nil { if err != nil {
return return
} }
payloadCipher, err = cipher.NewGCM(c) o.payloadCipher, err = cipher.NewGCM(c)
if err != nil { if err != nil {
return return
} }
obfuscator.maxOverhead = payloadCipher.Overhead()
case EncryptionMethodChaha20Poly1305: case EncryptionMethodChaha20Poly1305:
payloadCipher, err = chacha20poly1305.New(sessionKey[:]) o.payloadCipher, err = chacha20poly1305.New(sessionKey[:])
if err != nil { if err != nil {
return return
} }
obfuscator.maxOverhead = payloadCipher.Overhead()
default: default:
return obfuscator, errors.New("Unknown encryption method") return o, fmt.Errorf("unknown encryption method valued %v", encryptionMethod)
} }
if payloadCipher != nil { if o.payloadCipher != nil {
if payloadCipher.NonceSize() > frameHeaderLength { if o.payloadCipher.NonceSize() > frameHeaderLength {
return obfuscator, errors.New("payload AEAD's nonce size cannot be greater than size of frame header") return o, errors.New("payload AEAD's nonce size cannot be greater than size of frame header")
} }
} }
obfuscator.Obfs = MakeObfs(sessionKey, payloadCipher)
obfuscator.Deobfs = MakeDeobfs(sessionKey, payloadCipher)
return return
} }

View File

@ -1,83 +1,130 @@
package multiplex package multiplex
import ( import (
"bytes"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"golang.org/x/crypto/chacha20poly1305"
"math/rand" "math/rand"
"reflect" "reflect"
"testing" "testing"
"testing/quick" "testing/quick"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/chacha20poly1305"
) )
func TestGenerateObfs(t *testing.T) { func TestGenerateObfs(t *testing.T) {
var sessionKey [32]byte var sessionKey [32]byte
rand.Read(sessionKey[:]) rand.Read(sessionKey[:])
run := func(obfuscator Obfuscator, ct *testing.T) { run := func(o Obfuscator, t *testing.T) {
obfsBuf := make([]byte, 512) obfsBuf := make([]byte, 512)
_testFrame, _ := quick.Value(reflect.TypeOf(&Frame{}), rand.New(rand.NewSource(42))) _testFrame, _ := quick.Value(reflect.TypeOf(Frame{}), rand.New(rand.NewSource(42)))
testFrame := _testFrame.Interface().(*Frame) testFrame := _testFrame.Interface().(Frame)
i, err := obfuscator.Obfs(testFrame, obfsBuf, 0) i, err := o.obfuscate(&testFrame, obfsBuf, 0)
if err != nil { assert.NoError(t, err)
ct.Error("failed to obfs ", err)
return
}
var resultFrame Frame var resultFrame Frame
err = obfuscator.Deobfs(&resultFrame, obfsBuf[:i])
if err != nil { err = o.deobfuscate(&resultFrame, obfsBuf[:i])
ct.Error("failed to deobfs ", err) assert.NoError(t, err)
return assert.EqualValues(t, testFrame, resultFrame)
}
if !bytes.Equal(testFrame.Payload, resultFrame.Payload) || testFrame.StreamID != resultFrame.StreamID {
ct.Error("expecting", testFrame,
"got", resultFrame)
return
}
} }
t.Run("plain", func(t *testing.T) { t.Run("plain", func(t *testing.T) {
obfuscator, err := MakeObfuscator(EncryptionMethodPlain, sessionKey) o, err := MakeObfuscator(EncryptionMethodPlain, sessionKey)
if err != nil { assert.NoError(t, err)
t.Errorf("failed to generate obfuscator %v", err) run(o, t)
} else {
run(obfuscator, t)
}
}) })
t.Run("aes-256-gcm", func(t *testing.T) { t.Run("aes-256-gcm", func(t *testing.T) {
obfuscator, err := MakeObfuscator(EncryptionMethodAES256GCM, sessionKey) o, err := MakeObfuscator(EncryptionMethodAES256GCM, sessionKey)
if err != nil { assert.NoError(t, err)
t.Errorf("failed to generate obfuscator %v", err) run(o, t)
} else {
run(obfuscator, t)
}
}) })
t.Run("aes-128-gcm", func(t *testing.T) { t.Run("aes-128-gcm", func(t *testing.T) {
obfuscator, err := MakeObfuscator(EncryptionMethodAES128GCM, sessionKey) o, err := MakeObfuscator(EncryptionMethodAES128GCM, sessionKey)
if err != nil { assert.NoError(t, err)
t.Errorf("failed to generate obfuscator %v", err) run(o, t)
} else {
run(obfuscator, t)
}
}) })
t.Run("chacha20-poly1305", func(t *testing.T) { t.Run("chacha20-poly1305", func(t *testing.T) {
obfuscator, err := MakeObfuscator(EncryptionMethodChaha20Poly1305, sessionKey) o, err := MakeObfuscator(EncryptionMethodChaha20Poly1305, sessionKey)
if err != nil { assert.NoError(t, err)
t.Errorf("failed to generate obfuscator %v", err) run(o, t)
} else {
run(obfuscator, t)
}
}) })
t.Run("unknown encryption method", func(t *testing.T) { t.Run("unknown encryption method", func(t *testing.T) {
_, err := MakeObfuscator(0xff, sessionKey) _, err := MakeObfuscator(0xff, sessionKey)
if err == nil { assert.Error(t, err)
t.Errorf("unknown encryption mehtod error expected")
}
}) })
} }
func TestObfuscate(t *testing.T) {
var sessionKey [32]byte
rand.Read(sessionKey[:])
const testPayloadLen = 1024
testPayload := make([]byte, testPayloadLen)
rand.Read(testPayload)
f := Frame{
StreamID: 0,
Seq: 0,
Closing: 0,
Payload: testPayload,
}
runTest := func(t *testing.T, o Obfuscator) {
obfsBuf := make([]byte, testPayloadLen*2)
n, err := o.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
resultFrame := Frame{}
err = o.deobfuscate(&resultFrame, obfsBuf[:n])
assert.NoError(t, err)
assert.EqualValues(t, f, resultFrame)
}
t.Run("plain", func(t *testing.T) {
o := Obfuscator{
payloadCipher: nil,
sessionKey: sessionKey,
}
runTest(t, o)
})
t.Run("aes-128-gcm", func(t *testing.T) {
c, err := aes.NewCipher(sessionKey[:16])
assert.NoError(t, err)
payloadCipher, err := cipher.NewGCM(c)
assert.NoError(t, err)
o := Obfuscator{
payloadCipher: payloadCipher,
sessionKey: sessionKey,
}
runTest(t, o)
})
t.Run("aes-256-gcm", func(t *testing.T) {
c, err := aes.NewCipher(sessionKey[:])
assert.NoError(t, err)
payloadCipher, err := cipher.NewGCM(c)
assert.NoError(t, err)
o := Obfuscator{
payloadCipher: payloadCipher,
sessionKey: sessionKey,
}
runTest(t, o)
})
t.Run("chacha20-poly1305", func(t *testing.T) {
payloadCipher, err := chacha20poly1305.New(sessionKey[:])
assert.NoError(t, err)
o := Obfuscator{
payloadCipher: payloadCipher,
sessionKey: sessionKey,
}
runTest(t, o)
})
}
func BenchmarkObfs(b *testing.B) { func BenchmarkObfs(b *testing.B) {
testPayload := make([]byte, 1024) testPayload := make([]byte, 1024)
rand.Read(testPayload) rand.Read(testPayload)
@ -88,7 +135,7 @@ func BenchmarkObfs(b *testing.B) {
testPayload, testPayload,
} }
obfsBuf := make([]byte, defaultSendRecvBufSize) obfsBuf := make([]byte, len(testPayload)*2)
var key [32]byte var key [32]byte
rand.Read(key[:]) rand.Read(key[:])
@ -96,40 +143,53 @@ func BenchmarkObfs(b *testing.B) {
c, _ := aes.NewCipher(key[:]) c, _ := aes.NewCipher(key[:])
payloadCipher, _ := cipher.NewGCM(c) payloadCipher, _ := cipher.NewGCM(c)
obfs := MakeObfs(key, payloadCipher) obfuscator := Obfuscator{
payloadCipher: payloadCipher,
sessionKey: key,
}
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
obfs(testFrame, obfsBuf, 0) obfuscator.obfuscate(testFrame, obfsBuf, 0)
} }
}) })
b.Run("AES128GCM", func(b *testing.B) { b.Run("AES128GCM", func(b *testing.B) {
c, _ := aes.NewCipher(key[:16]) c, _ := aes.NewCipher(key[:16])
payloadCipher, _ := cipher.NewGCM(c) payloadCipher, _ := cipher.NewGCM(c)
obfs := MakeObfs(key, payloadCipher) obfuscator := Obfuscator{
payloadCipher: payloadCipher,
sessionKey: key,
}
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
obfs(testFrame, obfsBuf, 0) obfuscator.obfuscate(testFrame, obfsBuf, 0)
} }
}) })
b.Run("plain", func(b *testing.B) { b.Run("plain", func(b *testing.B) {
obfs := MakeObfs(key, nil) obfuscator := Obfuscator{
payloadCipher: nil,
sessionKey: key,
}
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
obfs(testFrame, obfsBuf, 0) obfuscator.obfuscate(testFrame, obfsBuf, 0)
} }
}) })
b.Run("chacha20Poly1305", func(b *testing.B) { b.Run("chacha20Poly1305", func(b *testing.B) {
payloadCipher, _ := chacha20poly1305.New(key[:16]) payloadCipher, _ := chacha20poly1305.New(key[:])
obfs := MakeObfs(key, payloadCipher) obfuscator := Obfuscator{
payloadCipher: payloadCipher,
sessionKey: key,
}
b.SetBytes(int64(len(testFrame.Payload))) b.SetBytes(int64(len(testFrame.Payload)))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
obfs(testFrame, obfsBuf, 0) obfuscator.obfuscate(testFrame, obfsBuf, 0)
} }
}) })
} }
@ -144,64 +204,73 @@ func BenchmarkDeobfs(b *testing.B) {
testPayload, testPayload,
} }
obfsBuf := make([]byte, defaultSendRecvBufSize) obfsBuf := make([]byte, len(testPayload)*2)
var key [32]byte var key [32]byte
rand.Read(key[:]) rand.Read(key[:])
b.Run("AES256GCM", func(b *testing.B) { b.Run("AES256GCM", func(b *testing.B) {
c, _ := aes.NewCipher(key[:]) c, _ := aes.NewCipher(key[:])
payloadCipher, _ := cipher.NewGCM(c) payloadCipher, _ := cipher.NewGCM(c)
obfuscator := Obfuscator{
payloadCipher: payloadCipher,
sessionKey: key,
}
obfs := MakeObfs(key, payloadCipher) n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
n, _ := obfs(testFrame, obfsBuf, 0)
deobfs := MakeDeobfs(key, payloadCipher)
frame := new(Frame) frame := new(Frame)
b.SetBytes(int64(n)) b.SetBytes(int64(n))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
deobfs(frame, obfsBuf[:n]) obfuscator.deobfuscate(frame, obfsBuf[:n])
} }
}) })
b.Run("AES128GCM", func(b *testing.B) { b.Run("AES128GCM", func(b *testing.B) {
c, _ := aes.NewCipher(key[:16]) c, _ := aes.NewCipher(key[:16])
payloadCipher, _ := cipher.NewGCM(c) payloadCipher, _ := cipher.NewGCM(c)
obfs := MakeObfs(key, payloadCipher) obfuscator := Obfuscator{
n, _ := obfs(testFrame, obfsBuf, 0) payloadCipher: payloadCipher,
deobfs := MakeDeobfs(key, payloadCipher) sessionKey: key,
}
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
frame := new(Frame) frame := new(Frame)
b.ResetTimer() b.ResetTimer()
b.SetBytes(int64(n)) b.SetBytes(int64(n))
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
deobfs(frame, obfsBuf[:n]) obfuscator.deobfuscate(frame, obfsBuf[:n])
} }
}) })
b.Run("plain", func(b *testing.B) { b.Run("plain", func(b *testing.B) {
obfs := MakeObfs(key, nil) obfuscator := Obfuscator{
n, _ := obfs(testFrame, obfsBuf, 0) payloadCipher: nil,
deobfs := MakeDeobfs(key, nil) sessionKey: key,
}
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
frame := new(Frame) frame := new(Frame)
b.ResetTimer() b.ResetTimer()
b.SetBytes(int64(n)) b.SetBytes(int64(n))
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
deobfs(frame, obfsBuf[:n]) obfuscator.deobfuscate(frame, obfsBuf[:n])
} }
}) })
b.Run("chacha20Poly1305", func(b *testing.B) { b.Run("chacha20Poly1305", func(b *testing.B) {
payloadCipher, _ := chacha20poly1305.New(key[:16]) payloadCipher, _ := chacha20poly1305.New(key[:])
obfs := MakeObfs(key, payloadCipher) obfuscator := Obfuscator{
n, _ := obfs(testFrame, obfsBuf, 0) payloadCipher: payloadCipher,
deobfs := MakeDeobfs(key, payloadCipher) sessionKey: key,
}
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
frame := new(Frame) frame := new(Frame)
b.ResetTimer() b.ResetTimer()
b.SetBytes(int64(n)) b.SetBytes(int64(n))
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
deobfs(frame, obfsBuf[:n]) obfuscator.deobfuscate(frame, obfsBuf[:n])
} }
}) })
} }

View File

@ -14,15 +14,11 @@ 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.
// If the buffer grows larger than what the system's memory can offer at the time of recvBuffer.Write, // If the buffer grows larger than what the system's memory can offer at the time of recvBuffer.Write,
// a panic will happen. // a panic will happen.
const recvBufferSizeLimit = defaultSendRecvBufSize << 12 const recvBufferSizeLimit = 1<<31 - 1

View File

@ -3,20 +3,20 @@ package multiplex
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
const ( const (
acceptBacklog = 1024 acceptBacklog = 1024
// TODO: will this be a signature?
defaultSendRecvBufSize = 20480
defaultInactivityTimeout = 30 * time.Second defaultInactivityTimeout = 30 * time.Second
defaultMaxOnWireSize = 1<<14 + 256 // https://tools.ietf.org/html/rfc8446#section-5.2
) )
var ErrBrokenSession = errors.New("broken session") var ErrBrokenSession = errors.New("broken session")
@ -24,8 +24,6 @@ var errRepeatSessionClosing = errors.New("trying to close a closed session")
var errRepeatStreamClosing = errors.New("trying to close a closed stream") var errRepeatStreamClosing = errors.New("trying to close a closed stream")
var errNoMultiplex = errors.New("a singleplexing session can have only one stream") var errNoMultiplex = errors.New("a singleplexing session can have only one stream")
type switchboardStrategy int
type SessionConfig struct { type SessionConfig struct {
Obfuscator Obfuscator
@ -40,12 +38,6 @@ type SessionConfig struct {
// maximum size of an obfuscated frame, including headers and overhead // maximum size of an obfuscated frame, including headers and overhead
MsgOnWireSizeLimit int MsgOnWireSizeLimit int
// StreamSendBufferSize sets the buffer size used to send data from a Stream (Stream.obfsBuf)
StreamSendBufferSize int
// ConnReceiveBufferSize sets the buffer size used to receive data from an underlying Conn (allocated in
// switchboard.deplex)
ConnReceiveBufferSize int
// InactivityTimeout sets the duration a Session waits while it has no active streams before it closes itself // InactivityTimeout sets the duration a Session waits while it has no active streams before it closes itself
InactivityTimeout time.Duration InactivityTimeout time.Duration
} }
@ -82,11 +74,17 @@ type Session struct {
closed uint32 closed uint32
terminalMsg atomic.Value terminalMsgSetter sync.Once
terminalMsg string
// the max size passed to Write calls before it splits it into multiple frames // the max size passed to Write calls before it splits it into multiple frames
// i.e. the max size a piece of data can fit into a Frame.Payload // i.e. the max size a piece of data can fit into a Frame.Payload
maxStreamUnitWrite int maxStreamUnitWrite int
// streamSendBufferSize sets the buffer size used to send data from a Stream (Stream.obfsBuf)
streamSendBufferSize int
// connReceiveBufferSize sets the buffer size used to receive data from an underlying Conn (allocated in
// switchboard.deplex)
connReceiveBufferSize int
} }
func MakeSession(id uint32, config SessionConfig) *Session { func MakeSession(id uint32, config SessionConfig) *Session {
@ -103,23 +101,19 @@ func MakeSession(id uint32, config SessionConfig) *Session {
if config.Valve == nil { if config.Valve == nil {
sesh.Valve = UNLIMITED_VALVE sesh.Valve = UNLIMITED_VALVE
} }
if config.StreamSendBufferSize <= 0 {
sesh.StreamSendBufferSize = defaultSendRecvBufSize
}
if config.ConnReceiveBufferSize <= 0 {
sesh.ConnReceiveBufferSize = defaultSendRecvBufSize
}
if config.MsgOnWireSizeLimit <= 0 { if config.MsgOnWireSizeLimit <= 0 {
sesh.MsgOnWireSizeLimit = defaultSendRecvBufSize - 1024 sesh.MsgOnWireSizeLimit = defaultMaxOnWireSize
} }
if config.InactivityTimeout == 0 { if config.InactivityTimeout == 0 {
sesh.InactivityTimeout = defaultInactivityTimeout sesh.InactivityTimeout = defaultInactivityTimeout
} }
// todo: validation. this must be smaller than StreamSendBufferSize
sesh.maxStreamUnitWrite = sesh.MsgOnWireSizeLimit - frameHeaderLength - sesh.Obfuscator.maxOverhead sesh.maxStreamUnitWrite = sesh.MsgOnWireSizeLimit - frameHeaderLength - maxExtraLen
sesh.streamSendBufferSize = sesh.MsgOnWireSizeLimit
sesh.connReceiveBufferSize = 20480 // for backwards compatibility
sesh.streamObfsBufPool = sync.Pool{New: func() interface{} { sesh.streamObfsBufPool = sync.Pool{New: func() interface{} {
b := make([]byte, sesh.StreamSendBufferSize) b := make([]byte, sesh.streamSendBufferSize)
return &b return &b
}} }}
@ -128,6 +122,10 @@ func MakeSession(id uint32, config SessionConfig) *Session {
return sesh return sesh
} }
func (sesh *Session) GetSessionKey() [32]byte {
return sesh.sessionKey
}
func (sesh *Session) streamCountIncr() uint32 { func (sesh *Session) streamCountIncr() uint32 {
return atomic.AddUint32(&sesh.activeStreamCount, 1) return atomic.AddUint32(&sesh.activeStreamCount, 1)
} }
@ -183,7 +181,7 @@ func (sesh *Session) closeStream(s *Stream, active bool) error {
if !atomic.CompareAndSwapUint32(&s.closed, 0, 1) { if !atomic.CompareAndSwapUint32(&s.closed, 0, 1) {
return fmt.Errorf("closing stream %v: %w", s.id, errRepeatStreamClosing) return fmt.Errorf("closing stream %v: %w", s.id, errRepeatStreamClosing)
} }
_ = s.getRecvBuf().Close() // recvBuf.Close should not return error _ = s.recvBuf.Close() // recvBuf.Close should not return error
if active { if active {
tmpBuf := sesh.streamObfsBufPool.Get().(*[]byte) tmpBuf := sesh.streamObfsBufPool.Get().(*[]byte)
@ -232,7 +230,7 @@ func (sesh *Session) recvDataFromRemote(data []byte) error {
frame := sesh.recvFramePool.Get().(*Frame) frame := sesh.recvFramePool.Get().(*Frame)
defer sesh.recvFramePool.Put(frame) defer sesh.recvFramePool.Put(frame)
err := sesh.Deobfs(frame, data) err := sesh.deobfuscate(frame, data)
if err != nil { if err != nil {
return fmt.Errorf("Failed to decrypt a frame for session %v: %v", sesh.id, err) return fmt.Errorf("Failed to decrypt a frame for session %v: %v", sesh.id, err)
} }
@ -267,16 +265,14 @@ func (sesh *Session) recvDataFromRemote(data []byte) error {
} }
func (sesh *Session) SetTerminalMsg(msg string) { func (sesh *Session) SetTerminalMsg(msg string) {
sesh.terminalMsg.Store(msg) log.Debug("terminal message set to " + msg)
sesh.terminalMsgSetter.Do(func() {
sesh.terminalMsg = msg
})
} }
func (sesh *Session) TerminalMsg() string { func (sesh *Session) TerminalMsg() string {
msg := sesh.terminalMsg.Load() return sesh.terminalMsg
if msg != nil {
return msg.(string)
} else {
return ""
}
} }
func (sesh *Session) closeSession() error { func (sesh *Session) closeSession() error {
@ -288,14 +284,12 @@ func (sesh *Session) closeSession() error {
sesh.streamsM.Lock() sesh.streamsM.Lock()
close(sesh.acceptCh) close(sesh.acceptCh)
for id, stream := range sesh.streams { for id, stream := range sesh.streams {
if stream == nil { if stream != nil && atomic.CompareAndSwapUint32(&stream.closed, 0, 1) {
continue _ = stream.recvBuf.Close() // will not block
}
atomic.StoreUint32(&stream.closed, 1)
_ = stream.getRecvBuf().Close() // will not block
delete(sesh.streams, id) delete(sesh.streams, id)
sesh.streamCountDecr() sesh.streamCountDecr()
} }
}
sesh.streamsM.Unlock() sesh.streamsM.Unlock()
return nil return nil
} }
@ -331,11 +325,11 @@ func (sesh *Session) Close() error {
Closing: closingSession, Closing: closingSession,
Payload: payload, Payload: payload,
} }
i, err := sesh.Obfs(f, *buf, frameHeaderLength) i, err := sesh.obfuscate(f, *buf, frameHeaderLength)
if err != nil { if err != nil {
return err return err
} }
_, err = sesh.sb.send((*buf)[:i], new(uint32)) _, err = sesh.sb.send((*buf)[:i], new(net.Conn))
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,3 +1,4 @@
//go:build gofuzz
// +build gofuzz // +build gofuzz
package multiplex package multiplex

View File

@ -2,87 +2,206 @@ package multiplex
import ( import (
"bytes" "bytes"
"github.com/cbeuw/connutil" "io"
"github.com/stretchr/testify/assert" "io/ioutil"
"math/rand" "math/rand"
"net"
"strconv" "strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
) )
var seshConfigs = map[string]SessionConfig{ var seshConfigs = map[string]SessionConfig{
"ordered": {}, "ordered": {},
"unordered": {Unordered: true}, "unordered": {Unordered: true},
} }
var encryptionMethods = map[string]byte{
"plain": EncryptionMethodPlain,
"aes-256-gcm": EncryptionMethodAES256GCM,
"aes-128-gcm": EncryptionMethodAES128GCM,
"chacha20poly1305": EncryptionMethodChaha20Poly1305,
}
const testPayloadLen = 1024 const testPayloadLen = 1024
const obfsBufLen = testPayloadLen * 2 const obfsBufLen = testPayloadLen * 2
func TestRecvDataFromRemote(t *testing.T) { func TestRecvDataFromRemote(t *testing.T) {
testPayload := make([]byte, testPayloadLen)
rand.Read(testPayload)
f := &Frame{
1,
0,
0,
testPayload,
}
obfsBuf := make([]byte, obfsBufLen)
var sessionKey [32]byte var sessionKey [32]byte
rand.Read(sessionKey[:]) rand.Read(sessionKey[:])
MakeObfuscatorUnwrap := func(method byte, sessionKey [32]byte) Obfuscator {
ret, err := MakeObfuscator(method, sessionKey)
if err != nil {
t.Fatalf("failed to make an obfuscator: %v", err)
}
return ret
}
encryptionMethods := map[string]Obfuscator{
"plain": MakeObfuscatorUnwrap(EncryptionMethodPlain, sessionKey),
"aes-gcm": MakeObfuscatorUnwrap(EncryptionMethodAES256GCM, sessionKey),
"chacha20-poly1305": MakeObfuscatorUnwrap(EncryptionMethodChaha20Poly1305, sessionKey),
}
for seshType, seshConfig := range seshConfigs { for seshType, seshConfig := range seshConfigs {
seshConfig := seshConfig seshConfig := seshConfig
t.Run(seshType, func(t *testing.T) { t.Run(seshType, func(t *testing.T) {
for method, obfuscator := range encryptionMethods { var err error
obfuscator := obfuscator seshConfig.Obfuscator, err = MakeObfuscator(EncryptionMethodPlain, sessionKey)
t.Run(method, func(t *testing.T) { if err != nil {
seshConfig.Obfuscator = obfuscator t.Fatalf("failed to make obfuscator: %v", err)
}
t.Run("initial frame", func(t *testing.T) {
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
n, err := sesh.Obfs(f, obfsBuf, 0) obfsBuf := make([]byte, obfsBufLen)
if err != nil { f := Frame{
t.Error(err) 1,
return 0,
0,
make([]byte, testPayloadLen),
} }
rand.Read(f.Payload)
n, err := sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n]) err = sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { assert.NoError(t, err)
t.Error(err)
return
}
stream, err := sesh.Accept() stream, err := sesh.Accept()
if err != nil { assert.NoError(t, err)
t.Error(err)
return
}
resultPayload := make([]byte, testPayloadLen) resultPayload := make([]byte, testPayloadLen)
_, err = stream.Read(resultPayload) _, err = stream.Read(resultPayload)
if err != nil { assert.NoError(t, err)
t.Error(err)
return assert.EqualValues(t, f.Payload, resultPayload)
})
t.Run("two frames in order", func(t *testing.T) {
sesh := MakeSession(0, seshConfig)
obfsBuf := make([]byte, obfsBufLen)
f := Frame{
1,
0,
0,
make([]byte, testPayloadLen),
} }
if !bytes.Equal(testPayload, resultPayload) { rand.Read(f.Payload)
t.Errorf("Expecting %x, got %x", testPayload, resultPayload) n, err := sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n])
assert.NoError(t, err)
stream, err := sesh.Accept()
assert.NoError(t, err)
resultPayload := make([]byte, testPayloadLen)
_, err = io.ReadFull(stream, resultPayload)
assert.NoError(t, err)
assert.EqualValues(t, f.Payload, resultPayload)
f.Seq += 1
rand.Read(f.Payload)
n, err = sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n])
assert.NoError(t, err)
_, err = io.ReadFull(stream, resultPayload)
assert.NoError(t, err)
assert.EqualValues(t, f.Payload, resultPayload)
})
t.Run("two frames in order", func(t *testing.T) {
sesh := MakeSession(0, seshConfig)
obfsBuf := make([]byte, obfsBufLen)
f := Frame{
1,
0,
0,
make([]byte, testPayloadLen),
} }
rand.Read(f.Payload)
n, err := sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n])
assert.NoError(t, err)
stream, err := sesh.Accept()
assert.NoError(t, err)
resultPayload := make([]byte, testPayloadLen)
_, err = io.ReadFull(stream, resultPayload)
assert.NoError(t, err)
assert.EqualValues(t, f.Payload, resultPayload)
f.Seq += 1
rand.Read(f.Payload)
n, err = sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n])
assert.NoError(t, err)
_, err = io.ReadFull(stream, resultPayload)
assert.NoError(t, err)
assert.EqualValues(t, f.Payload, resultPayload)
})
if seshType == "ordered" {
t.Run("frames out of order", func(t *testing.T) {
sesh := MakeSession(0, seshConfig)
obfsBuf := make([]byte, obfsBufLen)
f := Frame{
1,
0,
0,
nil,
}
// First frame
seq0 := make([]byte, testPayloadLen)
rand.Read(seq0)
f.Seq = 0
f.Payload = seq0
n, err := sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n])
assert.NoError(t, err)
// Third frame
seq2 := make([]byte, testPayloadLen)
rand.Read(seq2)
f.Seq = 2
f.Payload = seq2
n, err = sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n])
assert.NoError(t, err)
// Second frame
seq1 := make([]byte, testPayloadLen)
rand.Read(seq1)
f.Seq = 1
f.Payload = seq1
n, err = sesh.obfuscate(&f, obfsBuf, 0)
assert.NoError(t, err)
err = sesh.recvDataFromRemote(obfsBuf[:n])
assert.NoError(t, err)
// Expect things to receive in order
stream, err := sesh.Accept()
assert.NoError(t, err)
resultPayload := make([]byte, testPayloadLen)
// First
_, err = io.ReadFull(stream, resultPayload)
assert.NoError(t, err)
assert.EqualValues(t, seq0, resultPayload)
// Second
_, err = io.ReadFull(stream, resultPayload)
assert.NoError(t, err)
assert.EqualValues(t, seq1, resultPayload)
// Third
_, err = io.ReadFull(stream, resultPayload)
assert.NoError(t, err)
assert.EqualValues(t, seq2, resultPayload)
}) })
} }
}) })
} }
} }
@ -94,10 +213,9 @@ func TestRecvDataFromRemote_Closing_InOrder(t *testing.T) {
var sessionKey [32]byte var sessionKey [32]byte
rand.Read(sessionKey[:]) rand.Read(sessionKey[:])
obfuscator, _ := MakeObfuscator(EncryptionMethodPlain, sessionKey)
seshConfig := seshConfigs["ordered"] seshConfig := seshConfigs["ordered"]
seshConfig.Obfuscator = obfuscator seshConfig.Obfuscator, _ = MakeObfuscator(EncryptionMethodPlain, sessionKey)
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
f1 := &Frame{ f1 := &Frame{
@ -107,7 +225,7 @@ func TestRecvDataFromRemote_Closing_InOrder(t *testing.T) {
testPayload, testPayload,
} }
// create stream 1 // create stream 1
n, _ := sesh.Obfs(f1, obfsBuf, 0) n, _ := sesh.obfuscate(f1, obfsBuf, 0)
err := sesh.recvDataFromRemote(obfsBuf[:n]) err := sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { if err != nil {
t.Fatalf("receiving normal frame for stream 1: %v", err) t.Fatalf("receiving normal frame for stream 1: %v", err)
@ -129,7 +247,7 @@ func TestRecvDataFromRemote_Closing_InOrder(t *testing.T) {
closingNothing, closingNothing,
testPayload, testPayload,
} }
n, _ = sesh.Obfs(f2, obfsBuf, 0) n, _ = sesh.obfuscate(f2, obfsBuf, 0)
err = sesh.recvDataFromRemote(obfsBuf[:n]) err = sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { if err != nil {
t.Fatalf("receiving normal frame for stream 2: %v", err) t.Fatalf("receiving normal frame for stream 2: %v", err)
@ -151,7 +269,7 @@ func TestRecvDataFromRemote_Closing_InOrder(t *testing.T) {
closingStream, closingStream,
testPayload, testPayload,
} }
n, _ = sesh.Obfs(f1CloseStream, obfsBuf, 0) n, _ = sesh.obfuscate(f1CloseStream, obfsBuf, 0)
err = sesh.recvDataFromRemote(obfsBuf[:n]) err = sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { if err != nil {
t.Fatalf("receiving stream closing frame for stream 1: %v", err) t.Fatalf("receiving stream closing frame for stream 1: %v", err)
@ -180,7 +298,7 @@ func TestRecvDataFromRemote_Closing_InOrder(t *testing.T) {
} }
// close stream 1 again // close stream 1 again
n, _ = sesh.Obfs(f1CloseStream, obfsBuf, 0) n, _ = sesh.obfuscate(f1CloseStream, obfsBuf, 0)
err = sesh.recvDataFromRemote(obfsBuf[:n]) err = sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { if err != nil {
t.Fatalf("receiving stream closing frame for stream 1 %v", err) t.Fatalf("receiving stream closing frame for stream 1 %v", err)
@ -203,7 +321,7 @@ func TestRecvDataFromRemote_Closing_InOrder(t *testing.T) {
Closing: closingSession, Closing: closingSession,
Payload: testPayload, Payload: testPayload,
} }
n, _ = sesh.Obfs(fCloseSession, obfsBuf, 0) n, _ = sesh.obfuscate(fCloseSession, obfsBuf, 0)
err = sesh.recvDataFromRemote(obfsBuf[:n]) err = sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { if err != nil {
t.Fatalf("receiving session closing frame: %v", err) t.Fatalf("receiving session closing frame: %v", err)
@ -233,10 +351,9 @@ func TestRecvDataFromRemote_Closing_OutOfOrder(t *testing.T) {
var sessionKey [32]byte var sessionKey [32]byte
rand.Read(sessionKey[:]) rand.Read(sessionKey[:])
obfuscator, _ := MakeObfuscator(EncryptionMethodPlain, sessionKey)
seshConfig := seshConfigs["ordered"] seshConfig := seshConfigs["ordered"]
seshConfig.Obfuscator = obfuscator seshConfig.Obfuscator, _ = MakeObfuscator(EncryptionMethodPlain, sessionKey)
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
// receive stream 1 closing first // receive stream 1 closing first
@ -246,7 +363,7 @@ func TestRecvDataFromRemote_Closing_OutOfOrder(t *testing.T) {
closingStream, closingStream,
testPayload, testPayload,
} }
n, _ := sesh.Obfs(f1CloseStream, obfsBuf, 0) n, _ := sesh.obfuscate(f1CloseStream, obfsBuf, 0)
err := sesh.recvDataFromRemote(obfsBuf[:n]) err := sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { if err != nil {
t.Fatalf("receiving out of order stream closing frame for stream 1: %v", err) t.Fatalf("receiving out of order stream closing frame for stream 1: %v", err)
@ -268,7 +385,7 @@ func TestRecvDataFromRemote_Closing_OutOfOrder(t *testing.T) {
closingNothing, closingNothing,
testPayload, testPayload,
} }
n, _ = sesh.Obfs(f1, obfsBuf, 0) n, _ = sesh.obfuscate(f1, obfsBuf, 0)
err = sesh.recvDataFromRemote(obfsBuf[:n]) err = sesh.recvDataFromRemote(obfsBuf[:n])
if err != nil { if err != nil {
t.Fatalf("receiving normal frame for stream 1: %v", err) t.Fatalf("receiving normal frame for stream 1: %v", err)
@ -330,7 +447,7 @@ func TestParallelStreams(t *testing.T) {
wg.Add(1) wg.Add(1)
go func(frame *Frame) { go func(frame *Frame) {
obfsBuf := make([]byte, obfsBufLen) obfsBuf := make([]byte, obfsBufLen)
n, _ := sesh.Obfs(frame, obfsBuf, 0) n, _ := sesh.obfuscate(frame, obfsBuf, 0)
obfsBuf = obfsBuf[0:n] obfsBuf = obfsBuf[0:n]
err := sesh.recvDataFromRemote(obfsBuf) err := sesh.recvDataFromRemote(obfsBuf)
@ -415,10 +532,10 @@ func TestSession_timeoutAfter(t *testing.T) {
} }
} }
func BenchmarkRecvDataFromRemote_Ordered(b *testing.B) { func BenchmarkRecvDataFromRemote(b *testing.B) {
testPayload := make([]byte, testPayloadLen) testPayload := make([]byte, testPayloadLen)
rand.Read(testPayload) rand.Read(testPayload)
f := &Frame{ f := Frame{
1, 1,
0, 0,
0, 0,
@ -428,25 +545,25 @@ func BenchmarkRecvDataFromRemote_Ordered(b *testing.B) {
var sessionKey [32]byte var sessionKey [32]byte
rand.Read(sessionKey[:]) rand.Read(sessionKey[:])
table := map[string]byte{ const maxIter = 500_000 // run with -benchtime 500000x to avoid index out of bounds panic
"plain": EncryptionMethodPlain, for name, ep := range encryptionMethods {
"aes-gcm": EncryptionMethodAES256GCM,
"chacha20poly1305": EncryptionMethodChaha20Poly1305,
}
const maxIter = 100_000 // run with -benchtime 100000x to avoid index out of bounds panic
for name, ep := range table {
ep := ep ep := ep
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
seshConfig := seshConfigs["ordered"] for seshType, seshConfig := range seshConfigs {
obfuscator, _ := MakeObfuscator(ep, sessionKey) b.Run(seshType, func(b *testing.B) {
seshConfig.Obfuscator = obfuscator f := f
seshConfig.Obfuscator, _ = MakeObfuscator(ep, sessionKey)
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
go func() {
stream, _ := sesh.Accept()
io.Copy(ioutil.Discard, stream)
}()
binaryFrames := [maxIter][]byte{} binaryFrames := [maxIter][]byte{}
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
obfsBuf := make([]byte, obfsBufLen) obfsBuf := make([]byte, obfsBufLen)
n, _ := sesh.Obfs(f, obfsBuf, 0) n, _ := sesh.obfuscate(&f, obfsBuf, 0)
binaryFrames[i] = obfsBuf[:n] binaryFrames[i] = obfsBuf[:n]
f.Seq++ f.Seq++
} }
@ -458,27 +575,21 @@ func BenchmarkRecvDataFromRemote_Ordered(b *testing.B) {
} }
}) })
} }
})
}
} }
func BenchmarkMultiStreamWrite(b *testing.B) { func BenchmarkMultiStreamWrite(b *testing.B) {
var sessionKey [32]byte var sessionKey [32]byte
rand.Read(sessionKey[:]) rand.Read(sessionKey[:])
table := map[string]byte{
"plain": EncryptionMethodPlain,
"aes-gcm": EncryptionMethodAES256GCM,
"chacha20poly1305": EncryptionMethodChaha20Poly1305,
}
testPayload := make([]byte, testPayloadLen) testPayload := make([]byte, testPayloadLen)
for name, ep := range table { for name, ep := range encryptionMethods {
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
for seshType, seshConfig := range seshConfigs { for seshType, seshConfig := range seshConfigs {
seshConfig := seshConfig
b.Run(seshType, func(b *testing.B) { b.Run(seshType, func(b *testing.B) {
obfuscator, _ := MakeObfuscator(ep, sessionKey) seshConfig.Obfuscator, _ = MakeObfuscator(ep, sessionKey)
seshConfig.Obfuscator = obfuscator
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
sesh.AddConnection(connutil.Discard()) sesh.AddConnection(connutil.Discard())
b.ResetTimer() b.ResetTimer()
@ -494,3 +605,36 @@ func BenchmarkMultiStreamWrite(b *testing.B) {
}) })
} }
} }
func BenchmarkLatency(b *testing.B) {
var sessionKey [32]byte
rand.Read(sessionKey[:])
for name, ep := range encryptionMethods {
b.Run(name, func(b *testing.B) {
for seshType, seshConfig := range seshConfigs {
b.Run(seshType, func(b *testing.B) {
seshConfig.Obfuscator, _ = MakeObfuscator(ep, sessionKey)
clientSesh := MakeSession(0, seshConfig)
serverSesh := MakeSession(0, seshConfig)
c, s := net.Pipe()
clientSesh.AddConnection(c)
serverSesh.AddConnection(s)
buf := make([]byte, 64)
clientStream, _ := clientSesh.OpenStream()
clientStream.Write(buf)
serverStream, _ := serverSesh.Accept()
io.ReadFull(serverStream, buf)
b.ResetTimer()
for i := 0; i < b.N; i++ {
clientStream.Write(buf)
io.ReadFull(serverStream, buf)
}
})
}
})
}
}

View File

@ -6,9 +6,10 @@ import (
"net" "net"
"time" "time"
log "github.com/sirupsen/logrus"
"sync" "sync"
"sync/atomic" "sync/atomic"
log "github.com/sirupsen/logrus"
) )
var ErrBrokenStream = errors.New("broken stream") var ErrBrokenStream = errors.New("broken stream")
@ -23,9 +24,8 @@ type Stream struct {
session *Session session *Session
allocIdempot sync.Once
// a buffer (implemented as an asynchronous buffered pipe) to put data we've received from recvFrame but hasn't // a buffer (implemented as an asynchronous buffered pipe) to put data we've received from recvFrame but hasn't
// been read by the consumer through Read or WriteTo. Lazily allocated // been read by the consumer through Read or WriteTo.
recvBuf recvBuffer recvBuf recvBuffer
writingM sync.Mutex writingM sync.Mutex
@ -40,7 +40,7 @@ type Stream struct {
// recvBuffer (implemented by streamBuffer under ordered mode) will not receive out-of-order packets // recvBuffer (implemented by streamBuffer under ordered mode) will not receive out-of-order packets
// so it won't have to use its priority queue to sort it. // so it won't have to use its priority queue to sort it.
// This is not used in unordered connection mode // This is not used in unordered connection mode
assignedConnId uint32 assignedConn net.Conn
readFromTimeout time.Duration readFromTimeout time.Duration
} }
@ -56,25 +56,20 @@ func makeStream(sesh *Session, id uint32) *Stream {
}, },
} }
if sesh.Unordered {
stream.recvBuf = NewDatagramBufferedPipe()
} else {
stream.recvBuf = NewStreamBuffer()
}
return stream return stream
} }
func (s *Stream) isClosed() bool { return atomic.LoadUint32(&s.closed) == 1 } func (s *Stream) isClosed() bool { return atomic.LoadUint32(&s.closed) == 1 }
func (s *Stream) getRecvBuf() recvBuffer {
s.allocIdempot.Do(func() {
if s.session.Unordered {
s.recvBuf = NewDatagramBufferedPipe()
} else {
s.recvBuf = NewStreamBuffer()
}
})
return s.recvBuf
}
// receive a readily deobfuscated Frame so its payload can later be Read // receive a readily deobfuscated Frame so its payload can later be Read
func (s *Stream) recvFrame(frame *Frame) error { func (s *Stream) recvFrame(frame *Frame) error {
toBeClosed, err := s.getRecvBuf().Write(frame) toBeClosed, err := s.recvBuf.Write(frame)
if toBeClosed { if toBeClosed {
err = s.passiveClose() err = s.passiveClose()
if errors.Is(err, errRepeatStreamClosing) { if errors.Is(err, errRepeatStreamClosing) {
@ -93,7 +88,7 @@ func (s *Stream) Read(buf []byte) (n int, err error) {
return 0, nil return 0, nil
} }
n, err = s.getRecvBuf().Read(buf) n, err = s.recvBuf.Read(buf)
log.Tracef("%v read from stream %v with err %v", n, s.id, err) log.Tracef("%v read from stream %v with err %v", n, s.id, err)
if err == io.EOF { if err == io.EOF {
return n, ErrBrokenStream return n, ErrBrokenStream
@ -101,25 +96,14 @@ 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.getRecvBuf().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.Obfs(&s.writingFrame, buf, payloadOffsetInBuf) cipherTextLen, err := s.session.obfuscate(&s.writingFrame, buf, payloadOffsetInBuf)
s.writingFrame.Seq++ s.writingFrame.Seq++
if err != nil { if err != nil {
return err return err
} }
_, err = s.session.sb.send(buf[:cipherTextLen], &s.assignedConnId) _, err = s.session.sb.send(buf[:cipherTextLen], &s.assignedConn)
if err != nil { if err != nil {
if err == errBrokenSwitchboard { if err == errBrokenSwitchboard {
s.session.SetTerminalMsg(err.Error()) s.session.SetTerminalMsg(err.Error())
@ -215,8 +199,7 @@ 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.getRecvBuf().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.getRecvBuf().SetReadDeadline(t); return nil }
func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d } func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d }
var errNotImplemented = errors.New("Not implemented") var errNotImplemented = errors.New("Not implemented")

View File

@ -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()
@ -114,4 +109,3 @@ func (sb *streamBuffer) Close() error {
} }
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) }

View File

@ -3,6 +3,7 @@ package multiplex
import ( import (
"encoding/binary" "encoding/binary"
"io" "io"
//"log" //"log"
"sort" "sort"
"testing" "testing"

View File

@ -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()

View File

@ -1,10 +1,11 @@
package multiplex package multiplex
import ( import (
"bytes"
"math/rand" "math/rand"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
const readBlockTime = 500 * time.Millisecond const readBlockTime = 500 * time.Millisecond
@ -13,49 +14,15 @@ func TestPipeRW(t *testing.T) {
pipe := NewStreamBufferedPipe() pipe := NewStreamBufferedPipe()
b := []byte{0x01, 0x02, 0x03} b := []byte{0x01, 0x02, 0x03}
n, err := pipe.Write(b) n, err := pipe.Write(b)
if n != len(b) { assert.NoError(t, err, "simple write")
t.Error( assert.Equal(t, len(b), n, "number of bytes written")
"For", "number of bytes written",
"expecting", len(b),
"got", n,
)
return
}
if err != nil {
t.Error(
"For", "simple write",
"expecting", "nil error",
"got", err,
)
return
}
b2 := make([]byte, len(b)) b2 := make([]byte, len(b))
n, err = pipe.Read(b2) n, err = pipe.Read(b2)
if n != len(b) { assert.NoError(t, err, "simple read")
t.Error( assert.Equal(t, len(b), n, "number of bytes read")
"For", "number of bytes read",
"expecting", len(b),
"got", n,
)
return
}
if err != nil {
t.Error(
"For", "simple read",
"expecting", "nil error",
"got", err,
)
return
}
if !bytes.Equal(b, b2) {
t.Error(
"For", "simple read",
"expecting", b,
"got", b2,
)
}
assert.Equal(t, b, b2)
} }
func TestReadBlock(t *testing.T) { func TestReadBlock(t *testing.T) {
@ -67,30 +34,10 @@ func TestReadBlock(t *testing.T) {
}() }()
b2 := make([]byte, len(b)) b2 := make([]byte, len(b))
n, err := pipe.Read(b2) n, err := pipe.Read(b2)
if n != len(b) { assert.NoError(t, err, "blocked read")
t.Error( assert.Equal(t, len(b), n, "number of bytes read after block")
"For", "number of bytes read after block",
"expecting", len(b), assert.Equal(t, b, b2)
"got", n,
)
return
}
if err != nil {
t.Error(
"For", "blocked read",
"expecting", "nil error",
"got", err,
)
return
}
if !bytes.Equal(b, b2) {
t.Error(
"For", "blocked read",
"expecting", b,
"got", b2,
)
return
}
} }
func TestPartialRead(t *testing.T) { func TestPartialRead(t *testing.T) {
@ -99,54 +46,17 @@ func TestPartialRead(t *testing.T) {
pipe.Write(b) pipe.Write(b)
b1 := make([]byte, 1) b1 := make([]byte, 1)
n, err := pipe.Read(b1) n, err := pipe.Read(b1)
if n != len(b1) { assert.NoError(t, err, "partial read of 1")
t.Error( assert.Equal(t, len(b1), n, "number of bytes in partial read of 1")
"For", "number of bytes in partial read of 1",
"expecting", len(b1), assert.Equal(t, b[0], b1[0])
"got", n,
)
return
}
if err != nil {
t.Error(
"For", "partial read of 1",
"expecting", "nil error",
"got", err,
)
return
}
if b1[0] != b[0] {
t.Error(
"For", "partial read of 1",
"expecting", b[0],
"got", b1[0],
)
}
b2 := make([]byte, 2) b2 := make([]byte, 2)
n, err = pipe.Read(b2) n, err = pipe.Read(b2)
if n != len(b2) { assert.NoError(t, err, "partial read of 2")
t.Error( assert.Equal(t, len(b2), n, "number of bytes in partial read of 2")
"For", "number of bytes in partial read of 2",
"expecting", len(b2), assert.Equal(t, b[1:], b2)
"got", n,
)
}
if err != nil {
t.Error(
"For", "partial read of 2",
"expecting", "nil error",
"got", err,
)
return
}
if !bytes.Equal(b[1:], b2) {
t.Error(
"For", "partial read of 2",
"expecting", b[1:],
"got", b2,
)
return
}
} }
func TestReadAfterClose(t *testing.T) { func TestReadAfterClose(t *testing.T) {
@ -156,29 +66,10 @@ func TestReadAfterClose(t *testing.T) {
b2 := make([]byte, len(b)) b2 := make([]byte, len(b))
pipe.Close() pipe.Close()
n, err := pipe.Read(b2) n, err := pipe.Read(b2)
if n != len(b) { assert.NoError(t, err, "simple read")
t.Error( assert.Equal(t, len(b), n, "number of bytes read")
"For", "number of bytes read",
"expecting", len(b), assert.Equal(t, b, b2)
"got", n,
)
}
if err != nil {
t.Error(
"For", "simple read",
"expecting", "nil error",
"got", err,
)
return
}
if !bytes.Equal(b, b2) {
t.Error(
"For", "simple read",
"expecting", b,
"got", b2,
)
return
}
} }
func BenchmarkBufferedPipe_RW(b *testing.B) { func BenchmarkBufferedPipe_RW(b *testing.B) {

View File

@ -2,14 +2,14 @@ package multiplex
import ( import (
"bytes" "bytes"
"github.com/cbeuw/Cloak/internal/common"
"github.com/stretchr/testify/assert"
"io" "io"
"io/ioutil"
"math/rand" "math/rand"
"testing" "testing"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/stretchr/testify/assert"
"github.com/cbeuw/connutil" "github.com/cbeuw/connutil"
) )
@ -142,7 +142,7 @@ func TestStream_Close(t *testing.T) {
writingEnd := common.NewTLSConn(rawWritingEnd) writingEnd := common.NewTLSConn(rawWritingEnd)
obfsBuf := make([]byte, 512) obfsBuf := make([]byte, 512)
i, _ := sesh.Obfs(dataFrame, obfsBuf, 0) i, _ := sesh.obfuscate(dataFrame, obfsBuf, 0)
_, err := writingEnd.Write(obfsBuf[:i]) _, err := writingEnd.Write(obfsBuf[:i])
if err != nil { if err != nil {
t.Error("failed to write from remote end") t.Error("failed to write from remote end")
@ -185,7 +185,7 @@ func TestStream_Close(t *testing.T) {
writingEnd := common.NewTLSConn(rawWritingEnd) writingEnd := common.NewTLSConn(rawWritingEnd)
obfsBuf := make([]byte, 512) obfsBuf := make([]byte, 512)
i, err := sesh.Obfs(dataFrame, obfsBuf, 0) i, err := sesh.obfuscate(dataFrame, obfsBuf, 0)
if err != nil { if err != nil {
t.Errorf("failed to obfuscate frame %v", err) t.Errorf("failed to obfuscate frame %v", err)
} }
@ -207,7 +207,7 @@ func TestStream_Close(t *testing.T) {
testPayload, testPayload,
} }
i, err = sesh.Obfs(closingFrame, obfsBuf, 0) i, err = sesh.obfuscate(closingFrame, obfsBuf, 0)
if err != nil { if err != nil {
t.Errorf("failed to obfuscate frame %v", err) t.Errorf("failed to obfuscate frame %v", err)
} }
@ -223,7 +223,7 @@ func TestStream_Close(t *testing.T) {
testPayload, testPayload,
} }
i, err = sesh.Obfs(closingFrameDup, obfsBuf, 0) i, err = sesh.obfuscate(closingFrameDup, obfsBuf, 0)
if err != nil { if err != nil {
t.Errorf("failed to obfuscate frame %v", err) t.Errorf("failed to obfuscate frame %v", err)
} }
@ -275,7 +275,7 @@ func TestStream_Read(t *testing.T) {
obfsBuf := make([]byte, 512) obfsBuf := make([]byte, 512)
t.Run("Plain read", func(t *testing.T) { t.Run("Plain read", func(t *testing.T) {
f.StreamID = streamID f.StreamID = streamID
i, _ := sesh.Obfs(f, obfsBuf, 0) i, _ := sesh.obfuscate(f, obfsBuf, 0)
streamID++ streamID++
writingEnd.Write(obfsBuf[:i]) writingEnd.Write(obfsBuf[:i])
stream, err := sesh.Accept() stream, err := sesh.Accept()
@ -300,7 +300,7 @@ func TestStream_Read(t *testing.T) {
}) })
t.Run("Nil buf", func(t *testing.T) { t.Run("Nil buf", func(t *testing.T) {
f.StreamID = streamID f.StreamID = streamID
i, _ := sesh.Obfs(f, obfsBuf, 0) i, _ := sesh.obfuscate(f, obfsBuf, 0)
streamID++ streamID++
writingEnd.Write(obfsBuf[:i]) writingEnd.Write(obfsBuf[:i])
stream, _ := sesh.Accept() stream, _ := sesh.Accept()
@ -312,7 +312,7 @@ func TestStream_Read(t *testing.T) {
}) })
t.Run("Read after stream close", func(t *testing.T) { t.Run("Read after stream close", func(t *testing.T) {
f.StreamID = streamID f.StreamID = streamID
i, _ := sesh.Obfs(f, obfsBuf, 0) i, _ := sesh.obfuscate(f, obfsBuf, 0)
streamID++ streamID++
writingEnd.Write(obfsBuf[:i]) writingEnd.Write(obfsBuf[:i])
stream, _ := sesh.Accept() stream, _ := sesh.Accept()
@ -337,7 +337,7 @@ func TestStream_Read(t *testing.T) {
}) })
t.Run("Read after session close", func(t *testing.T) { t.Run("Read after session close", func(t *testing.T) {
f.StreamID = streamID f.StreamID = streamID
i, _ := sesh.Obfs(f, obfsBuf, 0) i, _ := sesh.obfuscate(f, obfsBuf, 0)
streamID++ streamID++
writingEnd.Write(obfsBuf[:i]) writingEnd.Write(obfsBuf[:i])
stream, _ := sesh.Accept() stream, _ := sesh.Accept()
@ -363,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),

View File

@ -2,17 +2,19 @@ package multiplex
import ( import (
"errors" "errors"
"github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"math/rand" "math/rand/v2"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
) )
type switchboardStrategy int
const ( const (
FIXED_CONN_MAPPING switchboardStrategy = iota fixedConnMapping switchboardStrategy = iota
UNIFORM_SPREAD uniformSpread
) )
// switchboard represents the connection pool. It is responsible for managing // switchboard represents the connection pool. It is responsible for managing
@ -28,30 +30,22 @@ type switchboard struct {
valve Valve valve Valve
strategy switchboardStrategy strategy switchboardStrategy
// map of connId to net.Conn
conns sync.Map conns sync.Map
numConns uint32 connsCount uint32
nextConnId uint32
randPool sync.Pool randPool sync.Pool
broken uint32 broken uint32
} }
func makeSwitchboard(sesh *Session) *switchboard { func makeSwitchboard(sesh *Session) *switchboard {
var strategy switchboardStrategy
if sesh.Unordered {
log.Debug("Connection is unordered")
strategy = UNIFORM_SPREAD
} else {
strategy = FIXED_CONN_MAPPING
}
sb := &switchboard{ sb := &switchboard{
session: sesh, session: sesh,
strategy: strategy, strategy: uniformSpread,
valve: sesh.Valve, valve: sesh.Valve,
nextConnId: 1,
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
@ -59,88 +53,81 @@ func makeSwitchboard(sesh *Session) *switchboard {
var errBrokenSwitchboard = errors.New("the switchboard is broken") var errBrokenSwitchboard = errors.New("the switchboard is broken")
func (sb *switchboard) connsCount() int {
return int(atomic.LoadUint32(&sb.numConns))
}
func (sb *switchboard) addConn(conn net.Conn) { func (sb *switchboard) addConn(conn net.Conn) {
connId := atomic.AddUint32(&sb.nextConnId, 1) - 1 connId := atomic.AddUint32(&sb.connsCount, 1) - 1
atomic.AddUint32(&sb.numConns, 1)
sb.conns.Store(connId, conn) sb.conns.Store(connId, conn)
go sb.deplex(connId, conn) go sb.deplex(conn)
} }
// a pointer to connId is passed here so that the switchboard can reassign it if that connId isn't usable // a pointer to assignedConn is passed here so that the switchboard can reassign it if that conn isn't usable
func (sb *switchboard) send(data []byte, connId *uint32) (n int, err error) { func (sb *switchboard) send(data []byte, assignedConn *net.Conn) (n int, err error) {
sb.valve.txWait(len(data)) sb.valve.txWait(len(data))
if atomic.LoadUint32(&sb.broken) == 1 || sb.connsCount() == 0 { if atomic.LoadUint32(&sb.broken) == 1 {
return 0, errBrokenSwitchboard return 0, errBrokenSwitchboard
} }
var conn net.Conn var conn net.Conn
switch sb.strategy { switch sb.strategy {
case UNIFORM_SPREAD: case uniformSpread:
_, conn, err = sb.pickRandConn() conn, err = sb.pickRandConn()
if err != nil { if err != nil {
return 0, errBrokenSwitchboard return 0, errBrokenSwitchboard
} }
case FIXED_CONN_MAPPING: n, err = conn.Write(data)
connI, ok := sb.conns.Load(*connId)
if ok {
conn = connI.(net.Conn)
} else {
var newConnId uint32
newConnId, conn, err = sb.pickRandConn()
if err != nil { if err != nil {
return 0, errBrokenSwitchboard sb.session.SetTerminalMsg("failed to send to remote " + err.Error())
sb.session.passiveClose()
return n, err
} }
*connId = newConnId 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
if conn == nil {
conn, err = sb.pickRandConn()
if err != nil {
sb.session.SetTerminalMsg("failed to pick a connection " + err.Error())
sb.session.passiveClose()
return 0, err
}
*assignedConn = conn
}
n, err = conn.Write(data)
if err != nil {
sb.session.SetTerminalMsg("failed to send to remote " + err.Error())
sb.session.passiveClose()
return n, err
} }
default: default:
return 0, errors.New("unsupported traffic distribution strategy") return 0, errors.New("unsupported traffic distribution strategy")
} }
n, err = conn.Write(data)
if err != nil {
sb.conns.Delete(*connId)
sb.session.SetTerminalMsg("failed to write to remote " + err.Error())
sb.session.passiveClose()
return n, err
}
sb.valve.AddTx(int64(n)) sb.valve.AddTx(int64(n))
return n, nil return n, nil
} }
// returns a random connId // returns a random conn. This function can be called concurrently.
func (sb *switchboard) pickRandConn() (uint32, net.Conn, error) { func (sb *switchboard) pickRandConn() (net.Conn, error) {
connCount := sb.connsCount() if atomic.LoadUint32(&sb.broken) == 1 {
if atomic.LoadUint32(&sb.broken) == 1 || connCount == 0 { return nil, errBrokenSwitchboard
return 0, nil, errBrokenSwitchboard }
connsCount := atomic.LoadUint32(&sb.connsCount)
if connsCount == 0 {
return nil, errBrokenSwitchboard
} }
// there is no guarantee that sb.conns still has the same amount of entries
// between the count loop and the pick loop
// so if the r > len(sb.conns) at the point of range call, the last visited element is picked
var id uint32
var conn net.Conn
randReader := sb.randPool.Get().(*rand.Rand) randReader := sb.randPool.Get().(*rand.Rand)
r := randReader.Intn(connCount) connId := randReader.Uint32N(connsCount)
sb.randPool.Put(randReader) sb.randPool.Put(randReader)
var c int
sb.conns.Range(func(connIdI, connI interface{}) bool { ret, ok := sb.conns.Load(connId)
if r == c { if !ok {
id = connIdI.(uint32) log.Errorf("failed to get conn %d", connId)
conn = connI.(net.Conn) return nil, errBrokenSwitchboard
return false
} }
c++ return ret.(net.Conn), nil
return true
})
// if len(sb.conns) is 0
if conn == nil {
return 0, nil, errBrokenSwitchboard
}
return id, conn, nil
} }
// actively triggered by session.Close() // actively triggered by session.Close()
@ -148,26 +135,24 @@ func (sb *switchboard) closeAll() {
if !atomic.CompareAndSwapUint32(&sb.broken, 0, 1) { if !atomic.CompareAndSwapUint32(&sb.broken, 0, 1) {
return return
} }
sb.conns.Range(func(key, connI interface{}) bool { atomic.StoreUint32(&sb.connsCount, 0)
conn := connI.(net.Conn) sb.conns.Range(func(_, conn interface{}) bool {
conn.Close() conn.(net.Conn).Close()
sb.conns.Delete(key) sb.conns.Delete(conn)
return true return true
}) })
} }
// deplex function costantly reads from a TCP connection // deplex function costantly reads from a TCP connection
func (sb *switchboard) deplex(connId uint32, conn net.Conn) { func (sb *switchboard) deplex(conn net.Conn) {
defer conn.Close() defer conn.Close()
buf := make([]byte, sb.session.ConnReceiveBufferSize) buf := make([]byte, sb.session.connReceiveBufferSize)
for { for {
n, err := conn.Read(buf) n, err := conn.Read(buf)
sb.valve.rxWait(n) sb.valve.rxWait(n)
sb.valve.AddRx(int64(n)) sb.valve.AddRx(int64(n))
if err != nil { if err != nil {
log.Debugf("a connection for session %v has closed: %v", sb.session.id, err) log.Debugf("a connection for session %v has closed: %v", sb.session.id, err)
sb.conns.Delete(connId)
atomic.AddUint32(&sb.numConns, ^uint32(0))
sb.session.SetTerminalMsg("a connection has dropped unexpectedly") sb.session.SetTerminalMsg("a connection has dropped unexpectedly")
sb.session.passiveClose() sb.session.passiveClose()
return return

View File

@ -1,12 +1,14 @@
package multiplex package multiplex
import ( import (
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
"math/rand" "math/rand"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
) )
func TestSwitchboard_Send(t *testing.T) { func TestSwitchboard_Send(t *testing.T) {
@ -14,14 +16,14 @@ func TestSwitchboard_Send(t *testing.T) {
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
hole0 := connutil.Discard() hole0 := connutil.Discard()
sesh.sb.addConn(hole0) sesh.sb.addConn(hole0)
connId, _, err := sesh.sb.pickRandConn() conn, err := sesh.sb.pickRandConn()
if err != nil { if err != nil {
t.Error("failed to get a random conn", err) t.Error("failed to get a random conn", err)
return return
} }
data := make([]byte, 1000) data := make([]byte, 1000)
rand.Read(data) rand.Read(data)
_, err = sesh.sb.send(data, &connId) _, err = sesh.sb.send(data, &conn)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -29,23 +31,23 @@ func TestSwitchboard_Send(t *testing.T) {
hole1 := connutil.Discard() hole1 := connutil.Discard()
sesh.sb.addConn(hole1) sesh.sb.addConn(hole1)
connId, _, err = sesh.sb.pickRandConn() conn, err = sesh.sb.pickRandConn()
if err != nil { if err != nil {
t.Error("failed to get a random conn", err) t.Error("failed to get a random conn", err)
return return
} }
_, err = sesh.sb.send(data, &connId) _, err = sesh.sb.send(data, &conn)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
} }
connId, _, err = sesh.sb.pickRandConn() conn, err = sesh.sb.pickRandConn()
if err != nil { if err != nil {
t.Error("failed to get a random conn", err) t.Error("failed to get a random conn", err)
return return
} }
_, err = sesh.sb.send(data, &connId) _, err = sesh.sb.send(data, &conn)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -71,7 +73,7 @@ func BenchmarkSwitchboard_Send(b *testing.B) {
seshConfig := SessionConfig{} seshConfig := SessionConfig{}
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
sesh.sb.addConn(hole) sesh.sb.addConn(hole)
connId, _, err := sesh.sb.pickRandConn() conn, err := sesh.sb.pickRandConn()
if err != nil { if err != nil {
b.Error("failed to get a random conn", err) b.Error("failed to get a random conn", err)
return return
@ -81,7 +83,7 @@ func BenchmarkSwitchboard_Send(b *testing.B) {
b.SetBytes(int64(len(data))) b.SetBytes(int64(len(data)))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
sesh.sb.send(data, &connId) sesh.sb.send(data, &conn)
} }
} }
@ -92,7 +94,7 @@ func TestSwitchboard_TxCredit(t *testing.T) {
sesh := MakeSession(0, seshConfig) sesh := MakeSession(0, seshConfig)
hole := connutil.Discard() hole := connutil.Discard()
sesh.sb.addConn(hole) sesh.sb.addConn(hole)
connId, _, err := sesh.sb.pickRandConn() conn, err := sesh.sb.pickRandConn()
if err != nil { if err != nil {
t.Error("failed to get a random conn", err) t.Error("failed to get a random conn", err)
return return
@ -100,10 +102,10 @@ func TestSwitchboard_TxCredit(t *testing.T) {
data := make([]byte, 1000) data := make([]byte, 1000)
rand.Read(data) rand.Read(data)
t.Run("FIXED CONN MAPPING", func(t *testing.T) { t.Run("fixed conn mapping", func(t *testing.T) {
*sesh.sb.valve.(*LimitedValve).tx = 0 *sesh.sb.valve.(*LimitedValve).tx = 0
sesh.sb.strategy = FIXED_CONN_MAPPING sesh.sb.strategy = fixedConnMapping
n, err := sesh.sb.send(data[:10], &connId) n, err := sesh.sb.send(data[:10], &conn)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -116,10 +118,10 @@ func TestSwitchboard_TxCredit(t *testing.T) {
t.Error("tx credit didn't increase by 10") t.Error("tx credit didn't increase by 10")
} }
}) })
t.Run("UNIFORM", func(t *testing.T) { t.Run("uniform spread", func(t *testing.T) {
*sesh.sb.valve.(*LimitedValve).tx = 0 *sesh.sb.valve.(*LimitedValve).tx = 0
sesh.sb.strategy = UNIFORM_SPREAD sesh.sb.strategy = uniformSpread
n, err := sesh.sb.send(data[:10], &connId) n, err := sesh.sb.send(data[:10], &conn)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -173,13 +175,13 @@ func TestSwitchboard_ConnsCount(t *testing.T) {
} }
wg.Wait() wg.Wait()
if sesh.sb.connsCount() != 1000 { if atomic.LoadUint32(&sesh.sb.connsCount) != 1000 {
t.Error("connsCount incorrect") t.Error("connsCount incorrect")
} }
sesh.sb.closeAll() sesh.sb.closeAll()
assert.Eventuallyf(t, func() bool { assert.Eventuallyf(t, func() bool {
return sesh.sb.connsCount() == 0 return atomic.LoadUint32(&sesh.sb.connsCount) == 0
}, time.Second, 10*time.Millisecond, "connsCount incorrect: %v", sesh.sb.connsCount()) }, time.Second, 10*time.Millisecond, "connsCount incorrect: %v", atomic.LoadUint32(&sesh.sb.connsCount))
} }

View File

@ -4,11 +4,11 @@ import (
"crypto" "crypto"
"errors" "errors"
"fmt" "fmt"
"io"
"net"
"github.com/cbeuw/Cloak/internal/common" "github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh" "github.com/cbeuw/Cloak/internal/ecdh"
"io"
"math/rand"
"net"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -45,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

View File

@ -5,6 +5,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common" "github.com/cbeuw/Cloak/internal/common"
) )
@ -163,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

View File

@ -1,9 +1,10 @@
package server package server
import ( import (
"github.com/cbeuw/Cloak/internal/server/usermanager"
"sync" "sync"
"github.com/cbeuw/Cloak/internal/server/usermanager"
mux "github.com/cbeuw/Cloak/internal/multiplex" mux "github.com/cbeuw/Cloak/internal/multiplex"
) )

View File

@ -3,12 +3,13 @@ package server
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/server/usermanager"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/server/usermanager"
) )
func getSeshConfig(unordered bool) mux.SessionConfig { func getSeshConfig(unordered bool) mux.SessionConfig {

View File

@ -5,9 +5,10 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -60,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
@ -83,10 +84,6 @@ func AuthFirstPacket(firstPacket []byte, transport Transport, sta *State) (info
err = fmt.Errorf("%w: %v", ErrBadDecryption, err) err = fmt.Errorf("%w: %v", ErrBadDecryption, err)
return return
} }
if _, ok := sta.ProxyBook[info.ProxyMethod]; !ok {
err = ErrBadProxyMethod
return
}
info.Transport = transport info.Transport = transport
return return
} }

View File

@ -4,10 +4,11 @@ import (
"crypto" "crypto"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh"
"testing" "testing"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh"
) )
func TestDecryptClientInfo(t *testing.T) { func TestDecryptClientInfo(t *testing.T) {

View File

@ -6,19 +6,22 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server/usermanager"
"io" "io"
"net" "net"
"net/http" "net/http"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server/usermanager"
mux "github.com/cbeuw/Cloak/internal/multiplex" mux "github.com/cbeuw/Cloak/internal/multiplex"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
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,
@ -123,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]
@ -213,6 +216,18 @@ func dispatchConnection(conn net.Conn, sta *State) {
return return
} }
if _, ok := sta.ProxyBook[ci.ProxyMethod]; !ok {
log.WithFields(log.Fields{
"remoteAddr": conn.RemoteAddr(),
"UID": b64(ci.UID),
"sessionId": ci.SessionId,
"proxyMethod": ci.ProxyMethod,
"encryptionMethod": ci.EncryptionMethod,
}).Error(ErrBadProxyMethod)
goWeb()
return
}
var user *ActiveUser var user *ActiveUser
if sta.IsBypass(ci.UID) { if sta.IsBypass(ci.UID) {
user, err = sta.Panel.GetBypassUser(ci.UID) user, err = sta.Panel.GetBypassUser(ci.UID)
@ -236,7 +251,7 @@ func dispatchConnection(conn net.Conn, sta *State) {
return return
} }
preparedConn, err := finishHandshake(conn, sesh.SessionKey, sta.WorldState.Rand) preparedConn, err := finishHandshake(conn, sesh.GetSessionKey(), sta.WorldState.Rand)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return return

View File

@ -2,12 +2,13 @@ package server
import ( import (
"encoding/hex" "encoding/hex"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
"io" "io"
"net" "net"
"testing" "testing"
"time" "time"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
) )
type rfpReturnValue struct { type rfpReturnValue struct {

View File

@ -1,13 +1,15 @@
//go:build gofuzz
// +build gofuzz // +build gofuzz
package server package server
import ( import (
"errors" "errors"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/connutil"
"net" "net"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/connutil"
) )
type rfpReturnValue_fuzz struct { type rfpReturnValue_fuzz struct {

View File

@ -5,13 +5,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server/usermanager"
"io/ioutil" "io/ioutil"
"net" "net"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server/usermanager"
) )
type RawConfig struct { type RawConfig struct {

View File

@ -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)

View File

@ -2,7 +2,7 @@ swagger: '2.0'
info: info:
description: | description: |
This is the API of Cloak server This is the API of Cloak server
version: 1.0.0 version: 0.0.2
title: Cloak Server title: Cloak Server
contact: contact:
email: cbeuw.andy@gmail.com email: cbeuw.andy@gmail.com
@ -12,8 +12,6 @@ info:
# host: petstore.swagger.io # host: petstore.swagger.io
# basePath: /v2 # basePath: /v2
tags: tags:
- name: admin
description: Endpoints used by the host administrators
- name: users - name: users
description: Operations related to user controls by admin description: Operations related to user controls by admin
# schemes: # schemes:
@ -22,7 +20,6 @@ paths:
/admin/users: /admin/users:
get: get:
tags: tags:
- admin
- users - users
summary: Show all users summary: Show all users
description: Returns an array of all UserInfo description: Returns an array of all UserInfo
@ -41,7 +38,6 @@ paths:
/admin/users/{UID}: /admin/users/{UID}:
get: get:
tags: tags:
- admin
- users - users
summary: Show userinfo by UID summary: Show userinfo by UID
description: Returns a UserInfo object description: Returns a UserInfo object
@ -68,7 +64,6 @@ paths:
description: internal error description: internal error
post: post:
tags: tags:
- admin
- users - users
summary: Updates the userinfo of the specified user, if the user does not exist, then a new user is created summary: Updates the userinfo of the specified user, if the user does not exist, then a new user is created
operationId: writeUserInfo operationId: writeUserInfo
@ -100,7 +95,6 @@ paths:
description: internal error description: internal error
delete: delete:
tags: tags:
- admin
- users - users
summary: Deletes a user summary: Deletes a user
operationId: deleteUser operationId: deleteUser

View File

@ -4,12 +4,13 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"github.com/stretchr/testify/assert"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
var mockUIDb64 = base64.URLEncoding.EncodeToString(mockUID) var mockUIDb64 = base64.URLEncoding.EncodeToString(mockUID)
@ -46,6 +47,36 @@ func TestWriteUserInfoHlr(t *testing.T) {
assert.Equalf(t, http.StatusCreated, rr.Code, "response body: %v", rr.Body) assert.Equalf(t, http.StatusCreated, rr.Code, "response body: %v", rr.Body)
}) })
t.Run("partial update", func(t *testing.T) {
req, err := http.NewRequest("POST", "/admin/users/"+mockUIDb64, bytes.NewBuffer(marshalled))
assert.NoError(t, err)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
partialUserInfo := UserInfo{
UID: mockUID,
SessionsCap: JustInt32(10),
}
partialMarshalled, _ := json.Marshal(partialUserInfo)
req, err = http.NewRequest("POST", "/admin/users/"+mockUIDb64, bytes.NewBuffer(partialMarshalled))
assert.NoError(t, err)
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
req, err = http.NewRequest("GET", "/admin/users/"+mockUIDb64, nil)
assert.NoError(t, err)
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var got UserInfo
err = json.Unmarshal(rr.Body.Bytes(), &got)
assert.NoError(t, err)
expected := mockUserInfo
expected.SessionsCap = partialUserInfo.SessionsCap
assert.EqualValues(t, expected, got)
})
t.Run("empty parameter", func(t *testing.T) { t.Run("empty parameter", func(t *testing.T) {
req, err := http.NewRequest("POST", "/admin/users/", bytes.NewBuffer(marshalled)) req, err := http.NewRequest("POST", "/admin/users/", bytes.NewBuffer(marshalled))
if err != nil { if err != nil {

View File

@ -2,6 +2,7 @@ package usermanager
import ( import (
"encoding/binary" "encoding/binary"
"github.com/cbeuw/Cloak/internal/common" "github.com/cbeuw/Cloak/internal/common"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
@ -127,6 +128,7 @@ func (manager *localManager) UploadStatus(uploads []StatusUpdate) ([]StatusRespo
"User no longer exists", "User no longer exists",
} }
responses = append(responses, resp) responses = append(responses, resp)
continue
} }
oldUp := int64(u64(bucket.Get([]byte("UpCredit")))) oldUp := int64(u64(bucket.Get([]byte("UpCredit"))))
@ -179,17 +181,20 @@ func (manager *localManager) ListAllUsers() (infos []UserInfo, err error) {
err = tx.ForEach(func(UID []byte, bucket *bolt.Bucket) error { err = tx.ForEach(func(UID []byte, bucket *bolt.Bucket) error {
var uinfo UserInfo var uinfo UserInfo
uinfo.UID = UID uinfo.UID = UID
uinfo.SessionsCap = int32(u32(bucket.Get([]byte("SessionsCap")))) uinfo.SessionsCap = JustInt32(int32(u32(bucket.Get([]byte("SessionsCap")))))
uinfo.UpRate = int64(u64(bucket.Get([]byte("UpRate")))) uinfo.UpRate = JustInt64(int64(u64(bucket.Get([]byte("UpRate")))))
uinfo.DownRate = int64(u64(bucket.Get([]byte("DownRate")))) uinfo.DownRate = JustInt64(int64(u64(bucket.Get([]byte("DownRate")))))
uinfo.UpCredit = int64(u64(bucket.Get([]byte("UpCredit")))) uinfo.UpCredit = JustInt64(int64(u64(bucket.Get([]byte("UpCredit")))))
uinfo.DownCredit = int64(u64(bucket.Get([]byte("DownCredit")))) uinfo.DownCredit = JustInt64(int64(u64(bucket.Get([]byte("DownCredit")))))
uinfo.ExpiryTime = int64(u64(bucket.Get([]byte("ExpiryTime")))) uinfo.ExpiryTime = JustInt64(int64(u64(bucket.Get([]byte("ExpiryTime")))))
infos = append(infos, uinfo) infos = append(infos, uinfo)
return nil return nil
}) })
return err return err
}) })
if infos == nil {
infos = []UserInfo{}
}
return return
} }
@ -200,41 +205,53 @@ func (manager *localManager) GetUserInfo(UID []byte) (uinfo UserInfo, err error)
return ErrUserNotFound return ErrUserNotFound
} }
uinfo.UID = UID uinfo.UID = UID
uinfo.SessionsCap = int32(u32(bucket.Get([]byte("SessionsCap")))) uinfo.SessionsCap = JustInt32(int32(u32(bucket.Get([]byte("SessionsCap")))))
uinfo.UpRate = int64(u64(bucket.Get([]byte("UpRate")))) uinfo.UpRate = JustInt64(int64(u64(bucket.Get([]byte("UpRate")))))
uinfo.DownRate = int64(u64(bucket.Get([]byte("DownRate")))) uinfo.DownRate = JustInt64(int64(u64(bucket.Get([]byte("DownRate")))))
uinfo.UpCredit = int64(u64(bucket.Get([]byte("UpCredit")))) uinfo.UpCredit = JustInt64(int64(u64(bucket.Get([]byte("UpCredit")))))
uinfo.DownCredit = int64(u64(bucket.Get([]byte("DownCredit")))) uinfo.DownCredit = JustInt64(int64(u64(bucket.Get([]byte("DownCredit")))))
uinfo.ExpiryTime = int64(u64(bucket.Get([]byte("ExpiryTime")))) uinfo.ExpiryTime = JustInt64(int64(u64(bucket.Get([]byte("ExpiryTime")))))
return nil return nil
}) })
return return
} }
func (manager *localManager) WriteUserInfo(uinfo UserInfo) (err error) { func (manager *localManager) WriteUserInfo(u UserInfo) (err error) {
err = manager.db.Update(func(tx *bolt.Tx) error { err = manager.db.Update(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(uinfo.UID) bucket, err := tx.CreateBucketIfNotExists(u.UID)
if err != nil { if err != nil {
return err return err
} }
if err = bucket.Put([]byte("SessionsCap"), i32ToB(int32(uinfo.SessionsCap))); err != nil { if u.SessionsCap != nil {
if err = bucket.Put([]byte("SessionsCap"), i32ToB(*u.SessionsCap)); err != nil {
return err return err
} }
if err = bucket.Put([]byte("UpRate"), i64ToB(uinfo.UpRate)); err != nil { }
if u.UpRate != nil {
if err = bucket.Put([]byte("UpRate"), i64ToB(*u.UpRate)); err != nil {
return err return err
} }
if err = bucket.Put([]byte("DownRate"), i64ToB(uinfo.DownRate)); err != nil { }
if u.DownRate != nil {
if err = bucket.Put([]byte("DownRate"), i64ToB(*u.DownRate)); err != nil {
return err return err
} }
if err = bucket.Put([]byte("UpCredit"), i64ToB(uinfo.UpCredit)); err != nil { }
if u.UpCredit != nil {
if err = bucket.Put([]byte("UpCredit"), i64ToB(*u.UpCredit)); err != nil {
return err return err
} }
if err = bucket.Put([]byte("DownCredit"), i64ToB(uinfo.DownCredit)); err != nil { }
if u.DownCredit != nil {
if err = bucket.Put([]byte("DownCredit"), i64ToB(*u.DownCredit)); err != nil {
return err return err
} }
if err = bucket.Put([]byte("ExpiryTime"), i64ToB(uinfo.ExpiryTime)); err != nil { }
if u.ExpiryTime != nil {
if err = bucket.Put([]byte("ExpiryTime"), i64ToB(*u.ExpiryTime)); err != nil {
return err return err
} }
}
return nil return nil
}) })
return return

View File

@ -2,7 +2,6 @@ package usermanager
import ( import (
"encoding/binary" "encoding/binary"
"github.com/cbeuw/Cloak/internal/common"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"os" "os"
@ -11,18 +10,21 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/stretchr/testify/assert"
) )
var mockUID = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} var mockUID = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
var mockWorldState = common.WorldOfTime(time.Unix(1, 0)) var mockWorldState = common.WorldOfTime(time.Unix(1, 0))
var mockUserInfo = UserInfo{ var mockUserInfo = UserInfo{
UID: mockUID, UID: mockUID,
SessionsCap: 0, SessionsCap: JustInt32(10),
UpRate: 0, UpRate: JustInt64(100),
DownRate: 0, DownRate: JustInt64(1000),
UpCredit: 0, UpCredit: JustInt64(10000),
DownCredit: 0, DownCredit: JustInt64(100000),
ExpiryTime: 100, ExpiryTime: JustInt64(1000000),
} }
func makeManager(t *testing.T) (mgr *localManager, cleaner func()) { func makeManager(t *testing.T) (mgr *localManager, cleaner func()) {
@ -43,6 +45,23 @@ func TestLocalManager_WriteUserInfo(t *testing.T) {
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
got, err := mgr.GetUserInfo(mockUID)
assert.NoError(t, err)
assert.EqualValues(t, mockUserInfo, got)
/* Partial update */
err = mgr.WriteUserInfo(UserInfo{
UID: mockUID,
SessionsCap: JustInt32(*mockUserInfo.SessionsCap + 1),
})
assert.NoError(t, err)
expected := mockUserInfo
expected.SessionsCap = JustInt32(*mockUserInfo.SessionsCap + 1)
got, err = mgr.GetUserInfo(mockUID)
assert.NoError(t, err)
assert.EqualValues(t, expected, got)
} }
func TestLocalManager_GetUserInfo(t *testing.T) { func TestLocalManager_GetUserInfo(t *testing.T) {
@ -63,7 +82,7 @@ func TestLocalManager_GetUserInfo(t *testing.T) {
t.Run("update a field", func(t *testing.T) { t.Run("update a field", func(t *testing.T) {
_ = mgr.WriteUserInfo(mockUserInfo) _ = mgr.WriteUserInfo(mockUserInfo)
updatedUserInfo := mockUserInfo updatedUserInfo := mockUserInfo
updatedUserInfo.SessionsCap = mockUserInfo.SessionsCap + 1 updatedUserInfo.SessionsCap = JustInt32(*mockUserInfo.SessionsCap + 1)
err := mgr.WriteUserInfo(updatedUserInfo) err := mgr.WriteUserInfo(updatedUserInfo)
if err != nil { if err != nil {
@ -103,15 +122,7 @@ func TestLocalManager_DeleteUser(t *testing.T) {
} }
} }
var validUserInfo = UserInfo{ var validUserInfo = mockUserInfo
UID: mockUID,
SessionsCap: 10,
UpRate: 100,
DownRate: 1000,
UpCredit: 10000,
DownCredit: 100000,
ExpiryTime: 1000000,
}
func TestLocalManager_AuthenticateUser(t *testing.T) { func TestLocalManager_AuthenticateUser(t *testing.T) {
var tmpDB, _ = ioutil.TempFile("", "ck_user_info") var tmpDB, _ = ioutil.TempFile("", "ck_user_info")
@ -128,7 +139,7 @@ func TestLocalManager_AuthenticateUser(t *testing.T) {
t.Error(err) t.Error(err)
} }
if upRate != validUserInfo.UpRate || downRate != validUserInfo.DownRate { if upRate != *validUserInfo.UpRate || downRate != *validUserInfo.DownRate {
t.Error("wrong up or down rate") t.Error("wrong up or down rate")
} }
}) })
@ -142,7 +153,7 @@ func TestLocalManager_AuthenticateUser(t *testing.T) {
t.Run("expired user", func(t *testing.T) { t.Run("expired user", func(t *testing.T) {
expiredUserInfo := validUserInfo expiredUserInfo := validUserInfo
expiredUserInfo.ExpiryTime = mockWorldState.Now().Add(-10 * time.Second).Unix() expiredUserInfo.ExpiryTime = JustInt64(mockWorldState.Now().Add(-10 * time.Second).Unix())
_ = mgr.WriteUserInfo(expiredUserInfo) _ = mgr.WriteUserInfo(expiredUserInfo)
@ -154,7 +165,7 @@ func TestLocalManager_AuthenticateUser(t *testing.T) {
t.Run("no credit", func(t *testing.T) { t.Run("no credit", func(t *testing.T) {
creditlessUserInfo := validUserInfo creditlessUserInfo := validUserInfo
creditlessUserInfo.UpCredit, creditlessUserInfo.DownCredit = -1, -1 creditlessUserInfo.UpCredit, creditlessUserInfo.DownCredit = JustInt64(-1), JustInt64(-1)
_ = mgr.WriteUserInfo(creditlessUserInfo) _ = mgr.WriteUserInfo(creditlessUserInfo)
@ -186,7 +197,7 @@ func TestLocalManager_AuthoriseNewSession(t *testing.T) {
t.Run("expired user", func(t *testing.T) { t.Run("expired user", func(t *testing.T) {
expiredUserInfo := validUserInfo expiredUserInfo := validUserInfo
expiredUserInfo.ExpiryTime = mockWorldState.Now().Add(-10 * time.Second).Unix() expiredUserInfo.ExpiryTime = JustInt64(mockWorldState.Now().Add(-10 * time.Second).Unix())
_ = mgr.WriteUserInfo(expiredUserInfo) _ = mgr.WriteUserInfo(expiredUserInfo)
err := mgr.AuthoriseNewSession(expiredUserInfo.UID, AuthorisationInfo{NumExistingSessions: 0}) err := mgr.AuthoriseNewSession(expiredUserInfo.UID, AuthorisationInfo{NumExistingSessions: 0})
@ -197,7 +208,7 @@ func TestLocalManager_AuthoriseNewSession(t *testing.T) {
t.Run("too many sessions", func(t *testing.T) { t.Run("too many sessions", func(t *testing.T) {
_ = mgr.WriteUserInfo(validUserInfo) _ = mgr.WriteUserInfo(validUserInfo)
err := mgr.AuthoriseNewSession(validUserInfo.UID, AuthorisationInfo{NumExistingSessions: int(validUserInfo.SessionsCap + 1)}) err := mgr.AuthoriseNewSession(validUserInfo.UID, AuthorisationInfo{NumExistingSessions: int(*validUserInfo.SessionsCap + 1)})
if err != ErrSessionsCapReached { if err != ErrSessionsCapReached {
t.Error("session cap not reached") t.Error("session cap not reached")
} }
@ -230,10 +241,10 @@ func TestLocalManager_UploadStatus(t *testing.T) {
t.Error(err) t.Error(err)
} }
if updatedUserInfo.UpCredit != validUserInfo.UpCredit-update.UpUsage { if *updatedUserInfo.UpCredit != *validUserInfo.UpCredit-update.UpUsage {
t.Error("up usage incorrect") t.Error("up usage incorrect")
} }
if updatedUserInfo.DownCredit != validUserInfo.DownCredit-update.DownUsage { if *updatedUserInfo.DownCredit != *validUserInfo.DownCredit-update.DownUsage {
t.Error("down usage incorrect") t.Error("down usage incorrect")
} }
}) })
@ -249,7 +260,7 @@ func TestLocalManager_UploadStatus(t *testing.T) {
UID: validUserInfo.UID, UID: validUserInfo.UID,
Active: true, Active: true,
NumSession: 1, NumSession: 1,
UpUsage: validUserInfo.UpCredit + 100, UpUsage: *validUserInfo.UpCredit + 100,
DownUsage: 0, DownUsage: 0,
Timestamp: mockWorldState.Now().Unix(), Timestamp: mockWorldState.Now().Unix(),
}, },
@ -261,19 +272,19 @@ func TestLocalManager_UploadStatus(t *testing.T) {
Active: true, Active: true,
NumSession: 1, NumSession: 1,
UpUsage: 0, UpUsage: 0,
DownUsage: validUserInfo.DownCredit + 100, DownUsage: *validUserInfo.DownCredit + 100,
Timestamp: mockWorldState.Now().Unix(), Timestamp: mockWorldState.Now().Unix(),
}, },
}, },
{"expired", {"expired",
UserInfo{ UserInfo{
UID: mockUID, UID: mockUID,
SessionsCap: 10, SessionsCap: JustInt32(10),
UpRate: 0, UpRate: JustInt64(0),
DownRate: 0, DownRate: JustInt64(0),
UpCredit: 0, UpCredit: JustInt64(0),
DownCredit: 0, DownCredit: JustInt64(0),
ExpiryTime: -1, ExpiryTime: JustInt64(-1),
}, },
StatusUpdate{ StatusUpdate{
UID: mockUserInfo.UID, UID: mockUserInfo.UID,
@ -318,12 +329,12 @@ func TestLocalManager_ListAllUsers(t *testing.T) {
rand.Read(randUID) rand.Read(randUID)
newUser := UserInfo{ newUser := UserInfo{
UID: randUID, UID: randUID,
SessionsCap: rand.Int31(), SessionsCap: JustInt32(rand.Int31()),
UpRate: rand.Int63(), UpRate: JustInt64(rand.Int63()),
DownRate: rand.Int63(), DownRate: JustInt64(rand.Int63()),
UpCredit: rand.Int63(), UpCredit: JustInt64(rand.Int63()),
DownCredit: rand.Int63(), DownCredit: JustInt64(rand.Int63()),
ExpiryTime: rand.Int63(), ExpiryTime: JustInt64(rand.Int63()),
} }
users = append(users, newUser) users = append(users, newUser)
wg.Add(1) wg.Add(1)

View File

@ -14,16 +14,23 @@ type StatusUpdate struct {
Timestamp int64 Timestamp int64
} }
type MaybeInt32 *int32
type MaybeInt64 *int64
type UserInfo struct { type UserInfo struct {
UID []byte UID []byte
SessionsCap int32 SessionsCap MaybeInt32
UpRate int64 UpRate MaybeInt64
DownRate int64 DownRate MaybeInt64
UpCredit int64 UpCredit MaybeInt64
DownCredit int64 DownCredit MaybeInt64
ExpiryTime int64 ExpiryTime MaybeInt64
} }
func JustInt32(v int32) MaybeInt32 { return &v }
func JustInt64(v int64) MaybeInt64 { return &v }
type StatusResponse struct { type StatusResponse struct {
UID []byte UID []byte
Action int Action int

View File

@ -15,7 +15,7 @@ func (v *Voidmanager) UploadStatus(updates []StatusUpdate) ([]StatusResponse, er
} }
func (v *Voidmanager) ListAllUsers() ([]UserInfo, error) { func (v *Voidmanager) ListAllUsers() ([]UserInfo, error) {
return nil, ErrMangerIsVoid return []UserInfo{}, ErrMangerIsVoid
} }
func (v *Voidmanager) GetUserInfo(UID []byte) (UserInfo, error) { func (v *Voidmanager) GetUserInfo(UID []byte) (UserInfo, error) {

View File

@ -1,8 +1,9 @@
package usermanager package usermanager
import ( import (
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
var v = &Voidmanager{} var v = &Voidmanager{}

View File

@ -2,11 +2,12 @@ package server
import ( import (
"encoding/base64" "encoding/base64"
"github.com/cbeuw/Cloak/internal/server/usermanager"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/cbeuw/Cloak/internal/server/usermanager"
mux "github.com/cbeuw/Cloak/internal/multiplex" mux "github.com/cbeuw/Cloak/internal/multiplex"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -2,12 +2,13 @@ package server
import ( import (
"encoding/base64" "encoding/base64"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server/usermanager"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"time" "time"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/server/usermanager"
) )
func TestUserPanel_BypassUser(t *testing.T) { func TestUserPanel_BypassUser(t *testing.T) {
@ -66,12 +67,12 @@ var mockUID = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
var mockWorldState = common.WorldOfTime(time.Unix(1, 0)) var mockWorldState = common.WorldOfTime(time.Unix(1, 0))
var validUserInfo = usermanager.UserInfo{ var validUserInfo = usermanager.UserInfo{
UID: mockUID, UID: mockUID,
SessionsCap: 10, SessionsCap: usermanager.JustInt32(10),
UpRate: 100, UpRate: usermanager.JustInt64(100),
DownRate: 1000, DownRate: usermanager.JustInt64(1000),
UpCredit: 10000, UpCredit: usermanager.JustInt64(10000),
DownCredit: 100000, DownCredit: usermanager.JustInt64(100000),
ExpiryTime: 1000000, ExpiryTime: usermanager.JustInt64(1000000),
} }
func TestUserPanel_GetUser(t *testing.T) { func TestUserPanel_GetUser(t *testing.T) {
@ -138,10 +139,10 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
} }
updatedUinfo, _ := mgr.GetUserInfo(validUserInfo.UID) updatedUinfo, _ := mgr.GetUserInfo(validUserInfo.UID)
if updatedUinfo.DownCredit != validUserInfo.DownCredit-1 { if *updatedUinfo.DownCredit != *validUserInfo.DownCredit-1 {
t.Error("down credit incorrect update") t.Error("down credit incorrect update")
} }
if updatedUinfo.UpCredit != validUserInfo.UpCredit-2 { if *updatedUinfo.UpCredit != *validUserInfo.UpCredit-2 {
t.Error("up credit incorrect update") t.Error("up credit incorrect update")
} }
@ -155,10 +156,10 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
} }
updatedUinfo, _ = mgr.GetUserInfo(validUserInfo.UID) updatedUinfo, _ = mgr.GetUserInfo(validUserInfo.UID)
if updatedUinfo.DownCredit != validUserInfo.DownCredit-(1+3) { if *updatedUinfo.DownCredit != *validUserInfo.DownCredit-(1+3) {
t.Error("down credit incorrect update") t.Error("down credit incorrect update")
} }
if updatedUinfo.UpCredit != validUserInfo.UpCredit-(2+4) { if *updatedUinfo.UpCredit != *validUserInfo.UpCredit-(2+4) {
t.Error("up credit incorrect update") t.Error("up credit incorrect update")
} }
}) })
@ -170,7 +171,7 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
t.Error(err) t.Error(err)
} }
user.valve.AddTx(validUserInfo.DownCredit + 100) user.valve.AddTx(*validUserInfo.DownCredit + 100)
panel.updateUsageQueue() panel.updateUsageQueue()
err = panel.commitUpdate() err = panel.commitUpdate()
if err != nil { if err != nil {
@ -182,7 +183,7 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
} }
updatedUinfo, _ := mgr.GetUserInfo(validUserInfo.UID) updatedUinfo, _ := mgr.GetUserInfo(validUserInfo.UID)
if updatedUinfo.DownCredit != -100 { if *updatedUinfo.DownCredit != -100 {
t.Error("down credit not updated correctly after the user has been terminated") t.Error("down credit not updated correctly after the user has been terminated")
} }
}) })

View File

@ -7,11 +7,12 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh"
"io" "io"
"net" "net"
"net/http" "net/http"
"github.com/cbeuw/Cloak/internal/common"
"github.com/cbeuw/Cloak/internal/ecdh"
) )
type WebSocket struct{} type WebSocket struct{}

View File

@ -2,11 +2,12 @@ package server
import ( import (
"errors" "errors"
"github.com/cbeuw/Cloak/internal/common"
"github.com/gorilla/websocket"
"net" "net"
"net/http" "net/http"
"github.com/cbeuw/Cloak/internal/common"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -2,8 +2,9 @@ package server
import ( import (
"bytes" "bytes"
"github.com/cbeuw/connutil"
"testing" "testing"
"github.com/cbeuw/connutil"
) )
func TestFirstBuffedConn_Read(t *testing.T) { func TestFirstBuffedConn_Read(t *testing.T) {

View File

@ -5,12 +5,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"github.com/cbeuw/Cloak/internal/client"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/server"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
"io" "io"
"math/rand" "math/rand"
"net" "net"
@ -18,6 +12,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/cbeuw/Cloak/internal/client"
"github.com/cbeuw/Cloak/internal/common"
mux "github.com/cbeuw/Cloak/internal/multiplex"
"github.com/cbeuw/Cloak/internal/server"
"github.com/cbeuw/connutil"
"github.com/stretchr/testify/assert"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -120,7 +121,7 @@ var singleplexTCPConfig = client.RawConfig{
RemotePort: "9999", RemotePort: "9999",
LocalHost: "127.0.0.1", LocalHost: "127.0.0.1",
LocalPort: "9999", LocalPort: "9999",
BrowserSig: "chrome", BrowserSig: "safari",
} }
func generateClientConfigs(rawConfig client.RawConfig, state common.WorldState) (client.LocalConnConfig, client.RemoteConnConfig, client.AuthInfo) { func generateClientConfigs(rawConfig client.RawConfig, state common.WorldState) (client.LocalConnConfig, client.RemoteConnConfig, client.AuthInfo) {
@ -321,7 +322,7 @@ func TestTCPSingleplex(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
const echoMsgLen = 16384 const echoMsgLen = 1 << 16
go serveTCPEcho(proxyFromCkServerL) go serveTCPEcho(proxyFromCkServerL)
proxyConn1, err := proxyToCkClientD.Dial("", "") proxyConn1, err := proxyToCkClientD.Dial("", "")

View File

@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
go get github.com/mitchellh/gox set -eu
go install github.com/mitchellh/gox@latest
mkdir -p release mkdir -p release
@ -12,13 +14,13 @@ if [ -z "$v" ]; then
fi fi
output="{{.Dir}}-{{.OS}}-{{.Arch}}-$v" output="{{.Dir}}-{{.OS}}-{{.Arch}}-$v"
osarch="!darwin/arm !darwin/arm64 !darwin/386" osarch="!darwin/arm !darwin/386"
echo "Compiling:" 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,9 @@ 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
sha256sum release/*

13
renovate.json Normal file
View File

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