mirror of https://github.com/cbeuw/Cloak
Compare commits
98 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c3d5470ef7 | |
|
|
8f629c7b2e | |
|
|
49357fc20b | |
|
|
8af137637e | |
|
|
51ed286f35 | |
|
|
5146ea8503 | |
|
|
7f9c17439f | |
|
|
c15fd730de | |
|
|
d06c208ace | |
|
|
9800e3685d | |
|
|
07aa197061 | |
|
|
cfdd5e6560 | |
|
|
64166bf580 | |
|
|
d229d8b3dc | |
|
|
8bbc7b08d3 | |
|
|
5cf975f596 | |
|
|
19c8cd1f89 | |
|
|
5867fa932b | |
|
|
bfaf46d2e9 | |
|
|
e362e81d19 | |
|
|
deb0d26c08 | |
|
|
3687087c67 | |
|
|
97a03139bc | |
|
|
b3c6426ac5 | |
|
|
dc2e83f75f | |
|
|
5988b4337d | |
|
|
de4dab6bf3 | |
|
|
d5da5d049c | |
|
|
392fc41de8 | |
|
|
3b449b64b3 | |
|
|
a848d2f7e5 | |
|
|
de1c7600c1 | |
|
|
767716b9be | |
|
|
1cc4a1f928 | |
|
|
82687d4419 | |
|
|
6b08af0c18 | |
|
|
c48a8800d6 | |
|
|
c5b31de753 | |
|
|
b9907c2e18 | |
|
|
6417e3393d | |
|
|
b3ec1ab3bc | |
|
|
eca5f13936 | |
|
|
fcb600efff | |
|
|
59919e5ec0 | |
|
|
d04366ec32 | |
|
|
bc67074610 | |
|
|
641f6b2a9c | |
|
|
646a323065 | |
|
|
b8f04c96c6 | |
|
|
0e0ec0e82b | |
|
|
6fe603c726 | |
|
|
11e42dd542 | |
|
|
e696b18187 | |
|
|
e305871d89 | |
|
|
9614fbc03d | |
|
|
f67ae6e644 | |
|
|
68f47f1d49 | |
|
|
9e2549c117 | |
|
|
39306cf930 | |
|
|
8e07491e98 | |
|
|
847b7e24bf | |
|
|
750340126a | |
|
|
611bad91fd | |
|
|
e157e73ade | |
|
|
67dba8c8fa | |
|
|
99c4c7730a | |
|
|
121c27fe1f | |
|
|
d73f464358 | |
|
|
9d5c663618 | |
|
|
387b76d426 | |
|
|
cfdff93a1c | |
|
|
8709bc0b2f | |
|
|
365f52a0e2 | |
|
|
88f41f755e | |
|
|
ae14e28999 | |
|
|
91106f3c49 | |
|
|
32494b8d86 | |
|
|
f27889af11 | |
|
|
a643402e11 | |
|
|
2bcb4a406d | |
|
|
603b614009 | |
|
|
b4d65d8a0e | |
|
|
8ab0c2d96b | |
|
|
4a2eac51fe | |
|
|
8dd4ae4bef | |
|
|
439b7f0eb3 | |
|
|
65443da872 | |
|
|
2c709f92df | |
|
|
eee8209195 | |
|
|
5cb54aa3c9 | |
|
|
fd695db044 | |
|
|
c60c6d8ddb | |
|
|
661db9d083 | |
|
|
f322f8b47f | |
|
|
8fbb2dd8cc | |
|
|
23b32b603f | |
|
|
e0b97db7cc | |
|
|
57bb437802 |
|
|
@ -7,11 +7,85 @@ jobs:
|
|||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
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 ./...
|
||||
- uses: codecov/codecov-action@v1
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: coverage.txt
|
||||
files: coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
compat-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
encryption-method: [ plain, chacha20-poly1305 ]
|
||||
num-conn: [ 0, 1, 4 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.24'
|
||||
- name: Build Cloak
|
||||
run: make
|
||||
- name: Create configs
|
||||
run: |
|
||||
mkdir config
|
||||
cat << EOF > config/ckclient.json
|
||||
{
|
||||
"Transport": "direct",
|
||||
"ProxyMethod": "iperf",
|
||||
"EncryptionMethod": "${{ matrix.encryption-method }}",
|
||||
"UID": "Q4GAXHVgnDLXsdTpw6bmoQ==",
|
||||
"PublicKey": "4dae/bF43FKGq+QbCc5P/E/MPM5qQeGIArjmJEHiZxc=",
|
||||
"ServerName": "cloudflare.com",
|
||||
"BrowserSig": "firefox",
|
||||
"NumConn": ${{ matrix.num-conn }}
|
||||
}
|
||||
EOF
|
||||
cat << EOF > config/ckserver.json
|
||||
{
|
||||
"ProxyBook": {
|
||||
"iperf": [
|
||||
"tcp",
|
||||
"127.0.0.1:5201"
|
||||
]
|
||||
},
|
||||
"BindAddr": [
|
||||
":8443"
|
||||
],
|
||||
"BypassUID": [
|
||||
"Q4GAXHVgnDLXsdTpw6bmoQ=="
|
||||
],
|
||||
"RedirAddr": "cloudflare.com",
|
||||
"PrivateKey": "AAaskZJRPIAbiuaRLHsvZPvE6gzOeSjg+ZRg1ENau0Y="
|
||||
}
|
||||
EOF
|
||||
- name: Start iperf3 server
|
||||
run: docker run -d --name iperf-server --network host ajoergensen/iperf3:latest --server
|
||||
- name: Test new client against old server
|
||||
run: |
|
||||
docker run -d --name old-cloak-server --network host -v $PWD/config:/go/Cloak/config cbeuw/cloak:latest build/ck-server -c config/ckserver.json --verbosity debug
|
||||
build/ck-client -c config/ckclient.json -s 127.0.0.1 -p 8443 --verbosity debug | tee new-cloak-client.log &
|
||||
docker run --network host ajoergensen/iperf3:latest --client 127.0.0.1 -p 1984
|
||||
docker stop old-cloak-server
|
||||
- name: Test old client against new server
|
||||
run: |
|
||||
build/ck-server -c config/ckserver.json --verbosity debug | tee new-cloak-server.log &
|
||||
docker run -d --name old-cloak-client --network host -v $PWD/config:/go/Cloak/config cbeuw/cloak:latest build/ck-client -c config/ckclient.json -s 127.0.0.1 -p 8443 --verbosity debug
|
||||
docker run --network host ajoergensen/iperf3:latest --client 127.0.0.1 -p 1984
|
||||
docker stop old-cloak-client
|
||||
- name: Dump docker logs
|
||||
if: always()
|
||||
run: |
|
||||
docker container logs iperf-server > iperf-server.log
|
||||
docker container logs old-cloak-server > old-cloak-server.log
|
||||
docker container logs old-cloak-client > old-cloak-client.log
|
||||
- name: Upload logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.encryption-method }}-${{ matrix.num-conn }}-conn-logs
|
||||
path: ./*.log
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: |
|
||||
export PATH=${PATH}:`go env GOPATH`/bin
|
||||
|
|
@ -20,3 +20,31 @@ jobs:
|
|||
files: release/*
|
||||
env:
|
||||
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,3 +2,5 @@ corpus/
|
|||
suppressions/
|
||||
crashers/
|
||||
*.zip
|
||||
.idea/
|
||||
build/
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
FROM golang:latest
|
||||
|
||||
RUN git clone https://github.com/cbeuw/Cloak.git
|
||||
WORKDIR Cloak
|
||||
RUN make
|
||||
58
README.md
58
README.md
|
|
@ -3,25 +3,30 @@
|
|||
[](https://goreportcard.com/report/github.com/cbeuw/Cloak)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SAUYKGSREP8GL&source=url)
|
||||
|
||||

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

|
||||
<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
|
||||
alongside traditional proxy tools like OpenVPN to evade deep-packet-inspection based censorship.
|
||||
Cloak is a [pluggable transport](https://datatracker.ietf.org/meeting/103/materials/slides-103-pearg-pt-slides-01) that enhances
|
||||
traditional proxy tools like OpenVPN to evade [sophisticated censorship](https://en.wikipedia.org/wiki/Deep_packet_inspection) and [data discrimination](https://en.wikipedia.org/wiki/Net_bias).
|
||||
|
||||
Cloak is not a standalone proxy program. Rather, it works by masquerading proxy tool's traffic as normal web browsing
|
||||
traffic. In contrast to traditional tools which have very prominent traffic "fingerprints", it's very difficult to
|
||||
precisely target Cloak with little false positives. This increases the collateral damage to censorship actions as
|
||||
Cloak is not a standalone proxy program. Rather, it works by masquerading proxied traffic as normal web browsing
|
||||
activities. In contrast to traditional tools which have very prominent traffic fingerprints and can be blocked by simple filtering rules,
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
UDP, such as Shadowsocks, OpenVPN and Tor. Multiple proxy servers can be running on the same server host machine and
|
||||
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 and
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
either connect to the host running Cloak server directly, or it can instead connect to a CDN edge server, which may be
|
||||
used by many other websites as well, thus further increases the collateral damage to censorship.
|
||||
Cloak also supports tunneling through an intermediary CDN server such as Amazon Cloudfront. Such services are so widely used,
|
||||
attempts to disrupt traffic to them can lead to very high collateral damage for the censor.
|
||||
|
||||
## 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).**
|
||||
|
||||
`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
|
||||
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
|
||||
the CDN server, this domain name will be used in the HTTP request to ask the CDN server to establish a WebSocket
|
||||
connection with this host.
|
||||
the CDN server, this domain name will be used in the `Host` header of the HTTP request to ask the CDN server to
|
||||
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
|
||||
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.
|
||||
|
||||
`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
|
||||
Cloak server. Zero or negative value disables it. Default is 0 (disabled). Warning: Enabling it might make your server
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//go:build go1.11
|
||||
// +build go1.11
|
||||
|
||||
package main
|
||||
|
|
@ -7,10 +8,11 @@ import (
|
|||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/client"
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
|
@ -73,6 +75,9 @@ func main() {
|
|||
log.Info("Starting standalone mode")
|
||||
}
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
})
|
||||
lvl, err := log.ParseLevel(*verbosity)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
@ -170,6 +175,11 @@ func main() {
|
|||
log.Infof("Listening on %v %v for %v client", network, localConfig.LocalAddr, authInfo.ProxyMethod)
|
||||
seshMaker = func() *mux.Session {
|
||||
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 limited to its UID.
|
||||
quad := make([]byte, 4)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !android
|
||||
// +build !android
|
||||
|
||||
package main
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build android
|
||||
// +build android
|
||||
|
||||
package main
|
||||
|
|
@ -28,9 +29,10 @@ import "C"
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !android
|
||||
// +build !android
|
||||
|
||||
package main
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//go:build android
|
||||
// +build android
|
||||
|
||||
package main
|
||||
|
||||
// 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 (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// In Android, once an app starts the VpnService, all outgoing traffic are routed by the system
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var version string
|
||||
|
|
@ -72,6 +73,10 @@ func main() {
|
|||
|
||||
var pluginMode bool
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
})
|
||||
|
||||
if os.Getenv("SS_LOCAL_HOST") != "" && os.Getenv("SS_LOCAL_PORT") != "" {
|
||||
pluginMode = true
|
||||
config = os.Getenv("SS_PLUGIN_OPTIONS")
|
||||
|
|
|
|||
|
|
@ -1,57 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseBindAddr(t *testing.T) {
|
||||
t.Run("port only", func(t *testing.T) {
|
||||
addrs, err := resolveBindAddr([]string{":443"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if addrs[0].String() != ":443" {
|
||||
t.Errorf("expected %v got %v", ":443", addrs[0].String())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ":443", addrs[0].String())
|
||||
})
|
||||
|
||||
t.Run("specific address", func(t *testing.T) {
|
||||
addrs, err := resolveBindAddr([]string{"192.168.1.123:443"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if addrs[0].String() != "192.168.1.123:443" {
|
||||
t.Errorf("expected %v got %v", "192.168.1.123:443", addrs[0].String())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "192.168.1.123:443", addrs[0].String())
|
||||
})
|
||||
|
||||
t.Run("ipv6", func(t *testing.T) {
|
||||
addrs, err := resolveBindAddr([]string{"[::]:443"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if addrs[0].String() != "[::]:443" {
|
||||
t.Errorf("expected %v got %v", "[::]:443", addrs[0].String())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[::]:443", addrs[0].String())
|
||||
})
|
||||
|
||||
t.Run("mixed", func(t *testing.T) {
|
||||
addrs, err := resolveBindAddr([]string{":80", "[::]:443"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
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())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ":80", addrs[0].String())
|
||||
assert.Equal(t, "[::]:443", addrs[1].String())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 1%
|
||||
project: off
|
||||
patch: off
|
||||
44
go.mod
44
go.mod
|
|
@ -1,24 +1,30 @@
|
|||
module github.com/cbeuw/Cloak
|
||||
|
||||
go 1.14
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/cbeuw/connutil v0.0.0-20200411160121-c5a5c4a9de14
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b // indirect
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/juju/ratelimit v1.0.1
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/mitchellh/gox v1.0.1 // indirect
|
||||
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
|
||||
github.com/stretchr/testify v1.6.1
|
||||
go.etcd.io/bbolt v1.3.4
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
|
||||
golang.org/x/tools v0.0.0-20201015182029-a5d9e455e9c4 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/refraction-networking/utls v1.8.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.etcd.io/bbolt v1.4.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
126
go.sum
126
go.sum
|
|
@ -1,85 +1,61 @@
|
|||
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
|
||||
github.com/cbeuw/connutil v0.0.0-20200411160121-c5a5c4a9de14 h1:bWJKlzTJR7C9DX0l1qhkTaP1lTEBWVDKhg8C/tNJqKg=
|
||||
github.com/cbeuw/connutil v0.0.0-20200411160121-c5a5c4a9de14/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs=
|
||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
|
||||
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
|
||||
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
|
||||
github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=
|
||||
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 h1:SL1K0QAuC1b54KoY1pjPWe6kSlsFHwK9/oC960fKrTY=
|
||||
github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
|
||||
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
|
||||
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||
github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8=
|
||||
github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
|
||||
github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
|
||||
github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
github.com/refraction-networking/utls v1.7.0/go.mod h1:lV0Gwc1/Fi+HYH8hOtgFRdHfKo4FKSn6+FdyOz9hRms=
|
||||
github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
|
||||
github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
|
||||
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
||||
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
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=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const appDataMaxLength = 16401
|
||||
|
|
@ -13,59 +14,125 @@ type clientHelloFields struct {
|
|||
random []byte
|
||||
sessionId []byte
|
||||
x25519KeyShare []byte
|
||||
sni []byte
|
||||
serverName string
|
||||
}
|
||||
|
||||
type browser interface {
|
||||
composeClientHello(clientHelloFields) []byte
|
||||
}
|
||||
type browser int
|
||||
|
||||
func makeServerName(serverName string) []byte {
|
||||
serverNameListLength := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(serverNameListLength, uint16(len(serverName)+3))
|
||||
serverNameType := []byte{0x00} // host_name
|
||||
serverNameLength := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(serverNameLength, uint16(len(serverName)))
|
||||
ret := make([]byte, 2+1+2+len(serverName))
|
||||
copy(ret[0:2], serverNameListLength)
|
||||
copy(ret[2:3], serverNameType)
|
||||
copy(ret[3:5], serverNameLength)
|
||||
copy(ret[5:], serverName)
|
||||
return ret
|
||||
}
|
||||
|
||||
// addExtensionRecord, add type, length to extension data
|
||||
func addExtRec(typ []byte, data []byte) []byte {
|
||||
length := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(length, uint16(len(data)))
|
||||
ret := make([]byte, 2+2+len(data))
|
||||
copy(ret[0:2], typ)
|
||||
copy(ret[2:4], length)
|
||||
copy(ret[4:], data)
|
||||
return ret
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
const (
|
||||
chrome = iota
|
||||
firefox
|
||||
safari
|
||||
)
|
||||
|
||||
type DirectTLS struct {
|
||||
*common.TLSConn
|
||||
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
|
||||
func (tls *DirectTLS) Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package client
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ package client
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/multiplex"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/multiplex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMakeAuthenticationPayload(t *testing.T) {
|
||||
|
|
@ -64,12 +66,8 @@ func TestMakeAuthenticationPayload(t *testing.T) {
|
|||
for _, tc := range tests {
|
||||
func() {
|
||||
payload, sharedSecret := makeAuthenticationPayload(tc.authInfo)
|
||||
if payload != tc.expPayload {
|
||||
t.Errorf("payload doesn't match:\nexp %v\ngot %v", tc.expPayload, payload)
|
||||
}
|
||||
if sharedSecret != tc.expSecret {
|
||||
t.Errorf("secret doesn't match:\nexp %x\ngot %x", tc.expPayload, payload)
|
||||
}
|
||||
assert.Equal(t, tc.expPayload, payload, "payload doesn't match")
|
||||
assert.Equal(t, tc.expSecret, sharedSecret, "shared secret doesn't match")
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -20,8 +21,10 @@ func MakeSession(connConfig RemoteConnConfig, authInfo AuthInfo, dialer common.D
|
|||
var wg sync.WaitGroup
|
||||
for i := 0; i < connConfig.NumConn; i++ {
|
||||
wg.Add(1)
|
||||
transportConfig := connConfig.Transport
|
||||
go func() {
|
||||
makeconn:
|
||||
transportConn := transportConfig.CreateTransport()
|
||||
remoteConn, err := dialer.Dial("tcp", connConfig.RemoteAddr)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
transportConn := connConfig.TransportMaker()
|
||||
sk, err := transportConn.Handshake(remoteConn, authInfo)
|
||||
if err != nil {
|
||||
transportConn.Close()
|
||||
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)
|
||||
|
||||
goto makeconn
|
||||
}
|
||||
// sessionKey given by each connection should be identical
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import (
|
|||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
)
|
||||
|
|
@ -30,12 +31,13 @@ type RawConfig struct {
|
|||
LocalPort string // jsonOptional
|
||||
RemoteHost string // jsonOptional
|
||||
RemotePort string // jsonOptional
|
||||
|
||||
AlternativeNames []string // jsonOptional
|
||||
// defaults set in ProcessRawConfig
|
||||
UDP bool // nullable
|
||||
BrowserSig string // nullable
|
||||
Transport string // nullable
|
||||
CDNOriginHost string // nullable
|
||||
CDNWsUrlPath string // nullable
|
||||
StreamTimeout int // nullable
|
||||
KeepAlive int // nullable
|
||||
}
|
||||
|
|
@ -45,12 +47,13 @@ type RemoteConnConfig struct {
|
|||
NumConn int
|
||||
KeepAlive time.Duration
|
||||
RemoteAddr string
|
||||
TransportMaker func() Transport
|
||||
Transport TransportConfig
|
||||
}
|
||||
|
||||
type LocalConnConfig struct {
|
||||
LocalAddr string
|
||||
Timeout time.Duration
|
||||
MockDomainList []string
|
||||
}
|
||||
|
||||
type AuthInfo struct {
|
||||
|
|
@ -94,6 +97,20 @@ func ssvToJson(ssv string) (ret []byte) {
|
|||
}
|
||||
key := sp[0]
|
||||
value := sp[1]
|
||||
if strings.HasPrefix(key, "AlternativeNames") {
|
||||
switch strings.Contains(value, ",") {
|
||||
case true:
|
||||
domains := strings.Split(value, ",")
|
||||
for index, domain := range domains {
|
||||
domains[index] = `"` + domain + `"`
|
||||
}
|
||||
value = strings.Join(domains, ",")
|
||||
ret = append(ret, []byte(`"`+key+`":[`+value+`],`)...)
|
||||
case false:
|
||||
ret = append(ret, []byte(`"`+key+`":["`+value+`"],`)...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// JSON doesn't like quotation marks around int and bool
|
||||
// This is extremely ugly but it's still better than writing a tokeniser
|
||||
if elem(key, unquoted) {
|
||||
|
|
@ -139,6 +156,17 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
|
|||
return nullErr("ServerName")
|
||||
}
|
||||
auth.MockDomain = raw.ServerName
|
||||
|
||||
var filteredAlternativeNames []string
|
||||
for _, alternativeName := range raw.AlternativeNames {
|
||||
if len(alternativeName) > 0 {
|
||||
filteredAlternativeNames = append(filteredAlternativeNames, alternativeName)
|
||||
}
|
||||
}
|
||||
raw.AlternativeNames = filteredAlternativeNames
|
||||
|
||||
local.MockDomainList = raw.AlternativeNames
|
||||
local.MockDomainList = append(local.MockDomainList, auth.MockDomain)
|
||||
if raw.ProxyMethod == "" {
|
||||
return nullErr("ServerName")
|
||||
}
|
||||
|
|
@ -163,8 +191,7 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
|
|||
switch strings.ToLower(raw.EncryptionMethod) {
|
||||
case "plain":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodPlain
|
||||
case "aes-gcm":
|
||||
case "aes-256-gcm":
|
||||
case "aes-gcm", "aes-256-gcm":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodAES256GCM
|
||||
case "aes-128-gcm":
|
||||
auth.EncryptionMethod = mux.EncryptionMethodAES128GCM
|
||||
|
|
@ -199,11 +226,13 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
|
|||
} else {
|
||||
cdnDomainPort = net.JoinHostPort(raw.CDNOriginHost, raw.RemotePort)
|
||||
}
|
||||
|
||||
remote.TransportMaker = func() Transport {
|
||||
return &WSOverTLS{
|
||||
cdnDomainPort: cdnDomainPort,
|
||||
if raw.CDNWsUrlPath == "" {
|
||||
raw.CDNWsUrlPath = "/"
|
||||
}
|
||||
|
||||
remote.Transport = TransportConfig{
|
||||
mode: "cdn",
|
||||
wsUrl: "ws://" + cdnDomainPort + raw.CDNWsUrlPath,
|
||||
}
|
||||
case "direct":
|
||||
fallthrough
|
||||
|
|
@ -211,18 +240,19 @@ func (raw *RawConfig) ProcessRawConfig(worldState common.WorldState) (local Loca
|
|||
var browser browser
|
||||
switch strings.ToLower(raw.BrowserSig) {
|
||||
case "firefox":
|
||||
browser = &Firefox{}
|
||||
browser = firefox
|
||||
case "safari":
|
||||
browser = safari
|
||||
case "chrome":
|
||||
fallthrough
|
||||
default:
|
||||
browser = &Chrome{}
|
||||
browser = chrome
|
||||
}
|
||||
remote.TransportMaker = func() Transport {
|
||||
return &DirectTLS{
|
||||
remote.Transport = TransportConfig{
|
||||
mode: "direct",
|
||||
browser: browser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KeepAlive
|
||||
if raw.KeepAlive <= 0 {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -8,3 +8,26 @@ type Transport interface {
|
|||
Handshake(rawConn net.Conn, authInfo AuthInfo) (sessionKey [32]byte, err error)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ import (
|
|||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/gorilla/websocket"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/gorilla/websocket"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
type WSOverTLS struct {
|
||||
*common.WebSocketConn
|
||||
cdnDomainPort string
|
||||
wsUrl string
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.Parse("ws://" + ws.cdnDomainPort)
|
||||
u, err := url.Parse(ws.wsUrl)
|
||||
if err != nil {
|
||||
return sessionKey, fmt.Errorf("failed to parse ws url: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
|
@ -52,8 +53,8 @@ func CryptoRandRead(buf []byte) {
|
|||
RandRead(rand.Reader, buf)
|
||||
}
|
||||
|
||||
func RandRead(randSource io.Reader, buf []byte) {
|
||||
_, err := randSource.Read(buf)
|
||||
func backoff(f func() error) {
|
||||
err := f()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -61,12 +62,36 @@ func RandRead(randSource io.Reader, buf []byte) {
|
|||
100 * time.Millisecond, 300 * time.Millisecond, 500 * time.Millisecond, 1 * time.Second,
|
||||
3 * time.Second, 5 * time.Second}
|
||||
for i := 0; i < 10; i++ {
|
||||
log.Errorf("Failed to get random bytes: %v. Retrying...", err)
|
||||
_, err = randSource.Read(buf)
|
||||
log.Errorf("Failed to get random: %v. Retrying...", err)
|
||||
err = f()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import (
|
|||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const gcmTagSize = 16
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package common
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"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) {
|
||||
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 = append(*writeBuf, byte(msgLen>>8), byte(msgLen&0xFF))
|
||||
*writeBuf = append(*writeBuf, in...)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ package common
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketConn implements io.ReadWriteCloser
|
||||
|
|
|
|||
|
|
@ -66,46 +66,6 @@ func (d *datagramBufferedPipe) Read(target []byte) (int, error) {
|
|||
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) {
|
||||
d.rwCond.L.Lock()
|
||||
defer d.rwCond.L.Unlock()
|
||||
|
|
@ -151,14 +111,6 @@ func (d *datagramBufferedPipe) SetReadDeadline(t time.Time) {
|
|||
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) {
|
||||
if d.timeoutTimer != nil {
|
||||
d.timeoutTimer.Stop()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package multiplex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDatagramBuffer_RW(t *testing.T) {
|
||||
|
|
@ -11,13 +12,7 @@ func TestDatagramBuffer_RW(t *testing.T) {
|
|||
t.Run("simple write", func(t *testing.T) {
|
||||
pipe := NewDatagramBufferedPipe()
|
||||
_, err := pipe.Write(&Frame{Payload: b})
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"expecting", "nil error",
|
||||
"got", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("simple read", func(t *testing.T) {
|
||||
|
|
@ -25,50 +20,18 @@ func TestDatagramBuffer_RW(t *testing.T) {
|
|||
_, _ = pipe.Write(&Frame{Payload: b})
|
||||
b2 := make([]byte, len(b))
|
||||
n, err := pipe.Read(b2)
|
||||
if n != len(b) {
|
||||
t.Error(
|
||||
"For", "number of bytes read",
|
||||
"expecting", len(b),
|
||||
"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
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(b), n)
|
||||
assert.Equal(t, b, b2)
|
||||
assert.Equal(t, 0, pipe.buf.Len(), "buf len is not 0 after finished reading")
|
||||
})
|
||||
|
||||
t.Run("writing closing frame", func(t *testing.T) {
|
||||
pipe := NewDatagramBufferedPipe()
|
||||
toBeClosed, err := pipe.Write(&Frame{Closing: closingStream})
|
||||
if !toBeClosed {
|
||||
t.Error("should be to be closed")
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"expecting", "nil error",
|
||||
"got", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
if !pipe.closed {
|
||||
t.Error("expecting closed pipe, not closed")
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, toBeClosed, "should be to be closed")
|
||||
assert.True(t, pipe.closed, "pipe should be closed")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -81,30 +44,9 @@ func TestDatagramBuffer_BlockingRead(t *testing.T) {
|
|||
}()
|
||||
b2 := make([]byte, len(b))
|
||||
n, err := pipe.Read(b2)
|
||||
if n != len(b) {
|
||||
t.Error(
|
||||
"For", "number of bytes read after block",
|
||||
"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
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(b), n, "number of bytes read after block is wrong")
|
||||
assert.Equal(t, b, b2)
|
||||
}
|
||||
|
||||
func TestDatagramBuffer_CloseThenRead(t *testing.T) {
|
||||
|
|
@ -114,27 +56,7 @@ func TestDatagramBuffer_CloseThenRead(t *testing.T) {
|
|||
b2 := make([]byte, len(b))
|
||||
pipe.Close()
|
||||
n, err := pipe.Read(b2)
|
||||
if n != len(b) {
|
||||
t.Error(
|
||||
"For", "number of bytes read",
|
||||
"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
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(b), n, "number of bytes read after block is wrong")
|
||||
assert.Equal(t, b, b2)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ package multiplex
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func serveEcho(l net.Listener) {
|
||||
|
|
@ -65,14 +66,15 @@ func makeSessionPair(numConn int) (*Session, *Session, []*connPair) {
|
|||
|
||||
func runEchoTest(t *testing.T, conns []net.Conn, msgLen int) {
|
||||
var wg sync.WaitGroup
|
||||
testData := make([]byte, msgLen)
|
||||
rand.Read(testData)
|
||||
|
||||
for _, conn := range conns {
|
||||
wg.Add(1)
|
||||
go func(conn net.Conn) {
|
||||
defer wg.Done()
|
||||
|
||||
testData := make([]byte, msgLen)
|
||||
rand.Read(testData)
|
||||
|
||||
// we cannot call t.Fatalf in concurrent contexts
|
||||
n, err := conn.Write(testData)
|
||||
if n != msgLen {
|
||||
|
|
@ -107,9 +109,7 @@ func TestMultiplex(t *testing.T) {
|
|||
streams := make([]net.Conn, numStreams)
|
||||
for i := 0; i < numStreams; i++ {
|
||||
stream, err := clientSession.OpenStream()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open stream: %v", err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
streams[i] = stream
|
||||
}
|
||||
|
||||
|
|
@ -122,18 +122,11 @@ func TestMultiplex(t *testing.T) {
|
|||
// close one stream
|
||||
closing, streams := streams[0], streams[1:]
|
||||
err := closing.Close()
|
||||
if err != nil {
|
||||
t.Errorf("couldn't close a stream")
|
||||
}
|
||||
assert.NoError(t, err, "couldn't close a stream")
|
||||
_, err = closing.Write([]byte{0})
|
||||
if err != ErrBrokenStream {
|
||||
t.Errorf("expecting error %v, got %v", ErrBrokenStream, err)
|
||||
}
|
||||
assert.Equal(t, ErrBrokenStream, err)
|
||||
_, err = closing.Read(make([]byte, 1))
|
||||
if err != ErrBrokenStream {
|
||||
t.Errorf("expecting error %v, got %v", ErrBrokenStream, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, ErrBrokenStream, err)
|
||||
}
|
||||
|
||||
func TestMux_StreamClosing(t *testing.T) {
|
||||
|
|
@ -145,20 +138,13 @@ func TestMux_StreamClosing(t *testing.T) {
|
|||
recvBuf := make([]byte, 128)
|
||||
toBeClosed, _ := clientSession.OpenStream()
|
||||
_, err := toBeClosed.Write(testData) // should be echoed back
|
||||
if err != nil {
|
||||
t.Errorf("can't write to stream: %v", err)
|
||||
}
|
||||
assert.NoError(t, err, "couldn't write to a stream")
|
||||
|
||||
_, err = io.ReadFull(toBeClosed, recvBuf[:1])
|
||||
if err != nil {
|
||||
t.Errorf("can't read anything before stream closed: %v", err)
|
||||
}
|
||||
assert.NoError(t, err, "can't read anything before stream closed")
|
||||
|
||||
_ = toBeClosed.Close()
|
||||
_, err = io.ReadFull(toBeClosed, recvBuf[1:])
|
||||
if err != nil {
|
||||
t.Errorf("can't read residual data on stream: %v", err)
|
||||
}
|
||||
if !bytes.Equal(testData, recvBuf) {
|
||||
t.Errorf("incorrect data read back")
|
||||
}
|
||||
assert.NoError(t, err, "can't read residual data on stream")
|
||||
assert.Equal(t, testData, recvBuf, "incorrect data read back")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package multiplex
|
|||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -11,14 +12,17 @@ import (
|
|||
"golang.org/x/crypto/salsa20"
|
||||
)
|
||||
|
||||
var u32 = binary.BigEndian.Uint32
|
||||
var u64 = binary.BigEndian.Uint64
|
||||
var putU32 = binary.BigEndian.PutUint32
|
||||
var putU64 = binary.BigEndian.PutUint64
|
||||
|
||||
const frameHeaderLength = 14
|
||||
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 (
|
||||
EncryptionMethodPlain = iota
|
||||
EncryptionMethodAES256GCM
|
||||
|
|
@ -30,9 +34,7 @@ const (
|
|||
type Obfuscator struct {
|
||||
payloadCipher cipher.AEAD
|
||||
|
||||
SessionKey [32]byte
|
||||
|
||||
maxOverhead int
|
||||
sessionKey [32]byte
|
||||
}
|
||||
|
||||
// obfuscate adds multiplexing headers, encrypt and add TLS header
|
||||
|
|
@ -40,7 +42,7 @@ func (o *Obfuscator) obfuscate(f *Frame, buf []byte, payloadOffsetInBuf int) (in
|
|||
// 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
|
||||
// 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
|
||||
// iv/nonce each time, relative to its key; that the frame header encryptor Salsa20 is given a unique
|
||||
|
|
@ -53,67 +55,57 @@ func (o *Obfuscator) obfuscate(f *Frame, buf []byte, payloadOffsetInBuf int) (in
|
|||
// to be large enough that they may never happen in reasonable time frames. Of course, different sessions
|
||||
// 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.
|
||||
// (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,
|
||||
// we pad the frame payload with random bytes until it reaches Salsa20's nonce size (8 bytes). Then we simply
|
||||
// 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.
|
||||
// we generate random bytes to be used as salsa20 nonce.
|
||||
payloadLen := len(f.Payload)
|
||||
if payloadLen == 0 {
|
||||
return 0, errors.New("payload cannot be empty")
|
||||
}
|
||||
var extraLen int
|
||||
if o.payloadCipher == nil {
|
||||
extraLen = salsa20NonceSize - payloadLen
|
||||
if extraLen < 0 {
|
||||
// if our payload is already greater than 8 bytes
|
||||
extraLen = 0
|
||||
}
|
||||
tagLen := 0
|
||||
if o.payloadCipher != nil {
|
||||
tagLen = o.payloadCipher.Overhead()
|
||||
} else {
|
||||
extraLen = o.payloadCipher.Overhead()
|
||||
if extraLen < salsa20NonceSize {
|
||||
return 0, errors.New("AEAD's Overhead cannot be fewer than 8 bytes")
|
||||
tagLen = salsa20NonceSize
|
||||
}
|
||||
// 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 {
|
||||
return 0, errors.New("obfs buffer too small")
|
||||
}
|
||||
// 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 payload is not at the correct location in buffer
|
||||
copy(payload, f.Payload)
|
||||
}
|
||||
|
||||
header := buf[:frameHeaderLength]
|
||||
putU32(header[0:4], f.StreamID)
|
||||
putU64(header[4:12], f.Seq)
|
||||
binary.BigEndian.PutUint32(header[0:4], f.StreamID)
|
||||
binary.BigEndian.PutUint64(header[4:12], f.Seq)
|
||||
header[12] = f.Closing
|
||||
header[13] = byte(extraLen)
|
||||
header[13] = byte(padLen + tagLen)
|
||||
|
||||
if o.payloadCipher == nil {
|
||||
if extraLen != 0 { // read nonce
|
||||
extra := buf[usefulLen-extraLen : usefulLen]
|
||||
common.CryptoRandRead(extra)
|
||||
// Random bytes for padding and nonce
|
||||
_, err := rand.Read(buf[frameHeaderLength+payloadLen : usefulLen])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to pad random: %w", err)
|
||||
}
|
||||
} else {
|
||||
|
||||
if o.payloadCipher != nil {
|
||||
o.payloadCipher.Seal(payload[:0], header[:o.payloadCipher.NonceSize()], payload, nil)
|
||||
}
|
||||
|
||||
nonce := buf[usefulLen-salsa20NonceSize : usefulLen]
|
||||
salsa20.XORKeyStream(header, header, nonce, &o.SessionKey)
|
||||
salsa20.XORKeyStream(header, header, nonce, &o.sessionKey)
|
||||
|
||||
return usefulLen, nil
|
||||
}
|
||||
|
|
@ -128,10 +120,10 @@ func (o *Obfuscator) deobfuscate(f *Frame, in []byte) error {
|
|||
pldWithOverHead := in[frameHeaderLength:] // payload + potential overhead
|
||||
|
||||
nonce := in[len(in)-salsa20NonceSize:]
|
||||
salsa20.XORKeyStream(header, header, nonce, &o.SessionKey)
|
||||
salsa20.XORKeyStream(header, header, nonce, &o.sessionKey)
|
||||
|
||||
streamID := u32(header[0:4])
|
||||
seq := u64(header[4:12])
|
||||
streamID := binary.BigEndian.Uint32(header[0:4])
|
||||
seq := binary.BigEndian.Uint64(header[4:12])
|
||||
closing := header[12]
|
||||
extraLen := header[13]
|
||||
|
||||
|
|
@ -163,50 +155,45 @@ func (o *Obfuscator) deobfuscate(f *Frame, in []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (obfuscator Obfuscator, err error) {
|
||||
obfuscator = Obfuscator{
|
||||
SessionKey: sessionKey,
|
||||
func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (o Obfuscator, err error) {
|
||||
o = Obfuscator{
|
||||
sessionKey: sessionKey,
|
||||
}
|
||||
var payloadCipher cipher.AEAD
|
||||
switch encryptionMethod {
|
||||
case EncryptionMethodPlain:
|
||||
payloadCipher = nil
|
||||
obfuscator.maxOverhead = salsa20NonceSize
|
||||
o.payloadCipher = nil
|
||||
case EncryptionMethodAES256GCM:
|
||||
var c cipher.Block
|
||||
c, err = aes.NewCipher(sessionKey[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
payloadCipher, err = cipher.NewGCM(c)
|
||||
o.payloadCipher, err = cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
obfuscator.maxOverhead = payloadCipher.Overhead()
|
||||
case EncryptionMethodAES128GCM:
|
||||
var c cipher.Block
|
||||
c, err = aes.NewCipher(sessionKey[:16])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
payloadCipher, err = cipher.NewGCM(c)
|
||||
o.payloadCipher, err = cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
obfuscator.maxOverhead = payloadCipher.Overhead()
|
||||
case EncryptionMethodChaha20Poly1305:
|
||||
payloadCipher, err = chacha20poly1305.New(sessionKey[:])
|
||||
o.payloadCipher, err = chacha20poly1305.New(sessionKey[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
obfuscator.maxOverhead = payloadCipher.Overhead()
|
||||
default:
|
||||
return obfuscator, fmt.Errorf("unknown encryption method valued %v", encryptionMethod)
|
||||
return o, fmt.Errorf("unknown encryption method valued %v", encryptionMethod)
|
||||
}
|
||||
|
||||
if payloadCipher != nil {
|
||||
if payloadCipher.NonceSize() > frameHeaderLength {
|
||||
return obfuscator, errors.New("payload AEAD's nonce size cannot be greater than size of frame header")
|
||||
if o.payloadCipher != nil {
|
||||
if o.payloadCipher.NonceSize() > frameHeaderLength {
|
||||
return o, errors.New("payload AEAD's nonce size cannot be greater than size of frame header")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,83 +1,130 @@
|
|||
package multiplex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
func TestGenerateObfs(t *testing.T) {
|
||||
var sessionKey [32]byte
|
||||
rand.Read(sessionKey[:])
|
||||
|
||||
run := func(obfuscator Obfuscator, ct *testing.T) {
|
||||
run := func(o Obfuscator, t *testing.T) {
|
||||
obfsBuf := make([]byte, 512)
|
||||
_testFrame, _ := quick.Value(reflect.TypeOf(&Frame{}), rand.New(rand.NewSource(42)))
|
||||
testFrame := _testFrame.Interface().(*Frame)
|
||||
i, err := obfuscator.obfuscate(testFrame, obfsBuf, 0)
|
||||
if err != nil {
|
||||
ct.Error("failed to obfs ", err)
|
||||
return
|
||||
}
|
||||
|
||||
_testFrame, _ := quick.Value(reflect.TypeOf(Frame{}), rand.New(rand.NewSource(42)))
|
||||
testFrame := _testFrame.Interface().(Frame)
|
||||
i, err := o.obfuscate(&testFrame, obfsBuf, 0)
|
||||
assert.NoError(t, err)
|
||||
var resultFrame Frame
|
||||
err = obfuscator.deobfuscate(&resultFrame, obfsBuf[:i])
|
||||
if err != nil {
|
||||
ct.Error("failed to deobfs ", err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(testFrame.Payload, resultFrame.Payload) || testFrame.StreamID != resultFrame.StreamID {
|
||||
ct.Error("expecting", testFrame,
|
||||
"got", resultFrame)
|
||||
return
|
||||
}
|
||||
|
||||
err = o.deobfuscate(&resultFrame, obfsBuf[:i])
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, testFrame, resultFrame)
|
||||
}
|
||||
|
||||
t.Run("plain", func(t *testing.T) {
|
||||
obfuscator, err := MakeObfuscator(EncryptionMethodPlain, sessionKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate obfuscator %v", err)
|
||||
} else {
|
||||
run(obfuscator, t)
|
||||
}
|
||||
o, err := MakeObfuscator(EncryptionMethodPlain, sessionKey)
|
||||
assert.NoError(t, err)
|
||||
run(o, t)
|
||||
})
|
||||
t.Run("aes-256-gcm", func(t *testing.T) {
|
||||
obfuscator, err := MakeObfuscator(EncryptionMethodAES256GCM, sessionKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate obfuscator %v", err)
|
||||
} else {
|
||||
run(obfuscator, t)
|
||||
}
|
||||
o, err := MakeObfuscator(EncryptionMethodAES256GCM, sessionKey)
|
||||
assert.NoError(t, err)
|
||||
run(o, t)
|
||||
})
|
||||
t.Run("aes-128-gcm", func(t *testing.T) {
|
||||
obfuscator, err := MakeObfuscator(EncryptionMethodAES128GCM, sessionKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate obfuscator %v", err)
|
||||
} else {
|
||||
run(obfuscator, t)
|
||||
}
|
||||
o, err := MakeObfuscator(EncryptionMethodAES128GCM, sessionKey)
|
||||
assert.NoError(t, err)
|
||||
run(o, t)
|
||||
})
|
||||
t.Run("chacha20-poly1305", func(t *testing.T) {
|
||||
obfuscator, err := MakeObfuscator(EncryptionMethodChaha20Poly1305, sessionKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate obfuscator %v", err)
|
||||
} else {
|
||||
run(obfuscator, t)
|
||||
}
|
||||
o, err := MakeObfuscator(EncryptionMethodChaha20Poly1305, sessionKey)
|
||||
assert.NoError(t, err)
|
||||
run(o, t)
|
||||
})
|
||||
t.Run("unknown encryption method", func(t *testing.T) {
|
||||
_, err := MakeObfuscator(0xff, sessionKey)
|
||||
if err == nil {
|
||||
t.Errorf("unknown encryption mehtod error expected")
|
||||
}
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
testPayload := make([]byte, 1024)
|
||||
rand.Read(testPayload)
|
||||
|
|
@ -88,7 +135,7 @@ func BenchmarkObfs(b *testing.B) {
|
|||
testPayload,
|
||||
}
|
||||
|
||||
obfsBuf := make([]byte, defaultSendRecvBufSize)
|
||||
obfsBuf := make([]byte, len(testPayload)*2)
|
||||
|
||||
var key [32]byte
|
||||
rand.Read(key[:])
|
||||
|
|
@ -98,8 +145,7 @@ func BenchmarkObfs(b *testing.B) {
|
|||
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: payloadCipher,
|
||||
SessionKey: key,
|
||||
maxOverhead: payloadCipher.Overhead(),
|
||||
sessionKey: key,
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(testFrame.Payload)))
|
||||
|
|
@ -114,8 +160,7 @@ func BenchmarkObfs(b *testing.B) {
|
|||
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: payloadCipher,
|
||||
SessionKey: key,
|
||||
maxOverhead: payloadCipher.Overhead(),
|
||||
sessionKey: key,
|
||||
}
|
||||
b.SetBytes(int64(len(testFrame.Payload)))
|
||||
b.ResetTimer()
|
||||
|
|
@ -126,8 +171,7 @@ func BenchmarkObfs(b *testing.B) {
|
|||
b.Run("plain", func(b *testing.B) {
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: nil,
|
||||
SessionKey: key,
|
||||
maxOverhead: salsa20NonceSize,
|
||||
sessionKey: key,
|
||||
}
|
||||
b.SetBytes(int64(len(testFrame.Payload)))
|
||||
b.ResetTimer()
|
||||
|
|
@ -140,8 +184,7 @@ func BenchmarkObfs(b *testing.B) {
|
|||
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: payloadCipher,
|
||||
SessionKey: key,
|
||||
maxOverhead: payloadCipher.Overhead(),
|
||||
sessionKey: key,
|
||||
}
|
||||
b.SetBytes(int64(len(testFrame.Payload)))
|
||||
b.ResetTimer()
|
||||
|
|
@ -161,7 +204,7 @@ func BenchmarkDeobfs(b *testing.B) {
|
|||
testPayload,
|
||||
}
|
||||
|
||||
obfsBuf := make([]byte, defaultSendRecvBufSize)
|
||||
obfsBuf := make([]byte, len(testPayload)*2)
|
||||
|
||||
var key [32]byte
|
||||
rand.Read(key[:])
|
||||
|
|
@ -170,8 +213,7 @@ func BenchmarkDeobfs(b *testing.B) {
|
|||
payloadCipher, _ := cipher.NewGCM(c)
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: payloadCipher,
|
||||
SessionKey: key,
|
||||
maxOverhead: payloadCipher.Overhead(),
|
||||
sessionKey: key,
|
||||
}
|
||||
|
||||
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
|
||||
|
|
@ -189,8 +231,7 @@ func BenchmarkDeobfs(b *testing.B) {
|
|||
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: payloadCipher,
|
||||
SessionKey: key,
|
||||
maxOverhead: payloadCipher.Overhead(),
|
||||
sessionKey: key,
|
||||
}
|
||||
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
|
||||
|
||||
|
|
@ -204,8 +245,7 @@ func BenchmarkDeobfs(b *testing.B) {
|
|||
b.Run("plain", func(b *testing.B) {
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: nil,
|
||||
SessionKey: key,
|
||||
maxOverhead: salsa20NonceSize,
|
||||
sessionKey: key,
|
||||
}
|
||||
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
|
||||
|
||||
|
|
@ -220,9 +260,8 @@ func BenchmarkDeobfs(b *testing.B) {
|
|||
payloadCipher, _ := chacha20poly1305.New(key[:])
|
||||
|
||||
obfuscator := Obfuscator{
|
||||
payloadCipher: nil,
|
||||
SessionKey: key,
|
||||
maxOverhead: payloadCipher.Overhead(),
|
||||
payloadCipher: payloadCipher,
|
||||
sessionKey: key,
|
||||
}
|
||||
|
||||
n, _ := obfuscator.obfuscate(testFrame, obfsBuf, 0)
|
||||
|
|
|
|||
|
|
@ -14,15 +14,11 @@ type recvBuffer interface {
|
|||
// Instead, it should behave as if it hasn't been closed. Closure is only relevant
|
||||
// when the buffer is empty.
|
||||
io.ReadCloser
|
||||
io.WriterTo
|
||||
Write(*Frame) (toBeClosed bool, err error)
|
||||
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.
|
||||
// If the buffer grows larger than what the system's memory can offer at the time of recvBuffer.Write,
|
||||
// a panic will happen.
|
||||
const recvBufferSizeLimit = defaultSendRecvBufSize << 12
|
||||
const recvBufferSizeLimit = 1<<31 - 1
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@ package multiplex
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
acceptBacklog = 1024
|
||||
// TODO: will this be a signature?
|
||||
defaultSendRecvBufSize = 20480
|
||||
defaultInactivityTimeout = 30 * time.Second
|
||||
defaultMaxOnWireSize = 1<<14 + 256 // https://tools.ietf.org/html/rfc8446#section-5.2
|
||||
)
|
||||
|
||||
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 errNoMultiplex = errors.New("a singleplexing session can have only one stream")
|
||||
|
||||
type switchboardStrategy int
|
||||
|
||||
type SessionConfig struct {
|
||||
Obfuscator
|
||||
|
||||
|
|
@ -40,12 +38,6 @@ type SessionConfig struct {
|
|||
// maximum size of an obfuscated frame, including headers and overhead
|
||||
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 time.Duration
|
||||
}
|
||||
|
|
@ -82,11 +74,17 @@ type Session struct {
|
|||
|
||||
closed uint32
|
||||
|
||||
terminalMsg atomic.Value
|
||||
terminalMsgSetter sync.Once
|
||||
terminalMsg string
|
||||
|
||||
// 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
|
||||
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 {
|
||||
|
|
@ -103,23 +101,19 @@ func MakeSession(id uint32, config SessionConfig) *Session {
|
|||
if config.Valve == nil {
|
||||
sesh.Valve = UNLIMITED_VALVE
|
||||
}
|
||||
if config.StreamSendBufferSize <= 0 {
|
||||
sesh.StreamSendBufferSize = defaultSendRecvBufSize
|
||||
}
|
||||
if config.ConnReceiveBufferSize <= 0 {
|
||||
sesh.ConnReceiveBufferSize = defaultSendRecvBufSize
|
||||
}
|
||||
if config.MsgOnWireSizeLimit <= 0 {
|
||||
sesh.MsgOnWireSizeLimit = defaultSendRecvBufSize - 1024
|
||||
sesh.MsgOnWireSizeLimit = defaultMaxOnWireSize
|
||||
}
|
||||
if config.InactivityTimeout == 0 {
|
||||
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{} {
|
||||
b := make([]byte, sesh.StreamSendBufferSize)
|
||||
b := make([]byte, sesh.streamSendBufferSize)
|
||||
return &b
|
||||
}}
|
||||
|
||||
|
|
@ -128,6 +122,10 @@ func MakeSession(id uint32, config SessionConfig) *Session {
|
|||
return sesh
|
||||
}
|
||||
|
||||
func (sesh *Session) GetSessionKey() [32]byte {
|
||||
return sesh.sessionKey
|
||||
}
|
||||
|
||||
func (sesh *Session) streamCountIncr() uint32 {
|
||||
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) {
|
||||
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 {
|
||||
tmpBuf := sesh.streamObfsBufPool.Get().(*[]byte)
|
||||
|
|
@ -267,16 +265,14 @@ func (sesh *Session) recvDataFromRemote(data []byte) error {
|
|||
}
|
||||
|
||||
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 {
|
||||
msg := sesh.terminalMsg.Load()
|
||||
if msg != nil {
|
||||
return msg.(string)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
return sesh.terminalMsg
|
||||
}
|
||||
|
||||
func (sesh *Session) closeSession() error {
|
||||
|
|
@ -288,14 +284,12 @@ func (sesh *Session) closeSession() error {
|
|||
sesh.streamsM.Lock()
|
||||
close(sesh.acceptCh)
|
||||
for id, stream := range sesh.streams {
|
||||
if stream == nil {
|
||||
continue
|
||||
}
|
||||
atomic.StoreUint32(&stream.closed, 1)
|
||||
_ = stream.getRecvBuf().Close() // will not block
|
||||
if stream != nil && atomic.CompareAndSwapUint32(&stream.closed, 0, 1) {
|
||||
_ = stream.recvBuf.Close() // will not block
|
||||
delete(sesh.streams, id)
|
||||
sesh.streamCountDecr()
|
||||
}
|
||||
}
|
||||
sesh.streamsM.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -335,7 +329,7 @@ func (sesh *Session) Close() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = sesh.sb.send((*buf)[:i], new(uint32))
|
||||
_, err = sesh.sb.send((*buf)[:i], new(net.Conn))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package multiplex
|
||||
|
|
|
|||
|
|
@ -2,87 +2,206 @@ package multiplex
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var seshConfigs = map[string]SessionConfig{
|
||||
"ordered": {},
|
||||
"unordered": {Unordered: true},
|
||||
}
|
||||
var encryptionMethods = map[string]byte{
|
||||
"plain": EncryptionMethodPlain,
|
||||
"aes-256-gcm": EncryptionMethodAES256GCM,
|
||||
"aes-128-gcm": EncryptionMethodAES128GCM,
|
||||
"chacha20poly1305": EncryptionMethodChaha20Poly1305,
|
||||
}
|
||||
|
||||
const testPayloadLen = 1024
|
||||
const obfsBufLen = testPayloadLen * 2
|
||||
|
||||
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
|
||||
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 {
|
||||
seshConfig := seshConfig
|
||||
t.Run(seshType, func(t *testing.T) {
|
||||
for method, obfuscator := range encryptionMethods {
|
||||
obfuscator := obfuscator
|
||||
t.Run(method, func(t *testing.T) {
|
||||
seshConfig.Obfuscator = obfuscator
|
||||
var err error
|
||||
seshConfig.Obfuscator, err = MakeObfuscator(EncryptionMethodPlain, sessionKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make obfuscator: %v", err)
|
||||
}
|
||||
t.Run("initial frame", func(t *testing.T) {
|
||||
sesh := MakeSession(0, seshConfig)
|
||||
n, err := sesh.obfuscate(f, obfsBuf, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
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])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
stream, err := sesh.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
resultPayload := make([]byte, testPayloadLen)
|
||||
_, err = stream.Read(resultPayload)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
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),
|
||||
}
|
||||
if !bytes.Equal(testPayload, resultPayload) {
|
||||
t.Errorf("Expecting %x, got %x", testPayload, resultPayload)
|
||||
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)
|
||||
})
|
||||
|
||||
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
|
||||
rand.Read(sessionKey[:])
|
||||
obfuscator, _ := MakeObfuscator(EncryptionMethodPlain, sessionKey)
|
||||
|
||||
seshConfig := seshConfigs["ordered"]
|
||||
seshConfig.Obfuscator = obfuscator
|
||||
seshConfig.Obfuscator, _ = MakeObfuscator(EncryptionMethodPlain, sessionKey)
|
||||
sesh := MakeSession(0, seshConfig)
|
||||
|
||||
f1 := &Frame{
|
||||
|
|
@ -233,10 +351,9 @@ func TestRecvDataFromRemote_Closing_OutOfOrder(t *testing.T) {
|
|||
|
||||
var sessionKey [32]byte
|
||||
rand.Read(sessionKey[:])
|
||||
obfuscator, _ := MakeObfuscator(EncryptionMethodPlain, sessionKey)
|
||||
|
||||
seshConfig := seshConfigs["ordered"]
|
||||
seshConfig.Obfuscator = obfuscator
|
||||
seshConfig.Obfuscator, _ = MakeObfuscator(EncryptionMethodPlain, sessionKey)
|
||||
sesh := MakeSession(0, seshConfig)
|
||||
|
||||
// receive stream 1 closing first
|
||||
|
|
@ -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)
|
||||
rand.Read(testPayload)
|
||||
f := &Frame{
|
||||
f := Frame{
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
|
|
@ -428,26 +545,25 @@ func BenchmarkRecvDataFromRemote_Ordered(b *testing.B) {
|
|||
var sessionKey [32]byte
|
||||
rand.Read(sessionKey[:])
|
||||
|
||||
table := map[string]byte{
|
||||
"plain": EncryptionMethodPlain,
|
||||
"aes-256-gcm": EncryptionMethodAES256GCM,
|
||||
"aes-128-gcm": EncryptionMethodAES128GCM,
|
||||
"chacha20poly1305": EncryptionMethodChaha20Poly1305,
|
||||
}
|
||||
|
||||
const maxIter = 100_000 // run with -benchtime 100000x to avoid index out of bounds panic
|
||||
for name, ep := range table {
|
||||
const maxIter = 500_000 // run with -benchtime 500000x to avoid index out of bounds panic
|
||||
for name, ep := range encryptionMethods {
|
||||
ep := ep
|
||||
b.Run(name, func(b *testing.B) {
|
||||
seshConfig := seshConfigs["ordered"]
|
||||
obfuscator, _ := MakeObfuscator(ep, sessionKey)
|
||||
seshConfig.Obfuscator = obfuscator
|
||||
for seshType, seshConfig := range seshConfigs {
|
||||
b.Run(seshType, func(b *testing.B) {
|
||||
f := f
|
||||
seshConfig.Obfuscator, _ = MakeObfuscator(ep, sessionKey)
|
||||
sesh := MakeSession(0, seshConfig)
|
||||
|
||||
go func() {
|
||||
stream, _ := sesh.Accept()
|
||||
io.Copy(ioutil.Discard, stream)
|
||||
}()
|
||||
|
||||
binaryFrames := [maxIter][]byte{}
|
||||
for i := 0; i < maxIter; i++ {
|
||||
obfsBuf := make([]byte, obfsBufLen)
|
||||
n, _ := sesh.obfuscate(f, obfsBuf, 0)
|
||||
n, _ := sesh.obfuscate(&f, obfsBuf, 0)
|
||||
binaryFrames[i] = obfsBuf[:n]
|
||||
f.Seq++
|
||||
}
|
||||
|
|
@ -459,28 +575,21 @@ func BenchmarkRecvDataFromRemote_Ordered(b *testing.B) {
|
|||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMultiStreamWrite(b *testing.B) {
|
||||
var sessionKey [32]byte
|
||||
rand.Read(sessionKey[:])
|
||||
|
||||
table := map[string]byte{
|
||||
"plain": EncryptionMethodPlain,
|
||||
"aes-256-gcm": EncryptionMethodAES256GCM,
|
||||
"aes-128-gcm": EncryptionMethodAES128GCM,
|
||||
"chacha20poly1305": EncryptionMethodChaha20Poly1305,
|
||||
}
|
||||
|
||||
testPayload := make([]byte, testPayloadLen)
|
||||
|
||||
for name, ep := range table {
|
||||
for name, ep := range encryptionMethods {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
for seshType, seshConfig := range seshConfigs {
|
||||
seshConfig := seshConfig
|
||||
b.Run(seshType, func(b *testing.B) {
|
||||
obfuscator, _ := MakeObfuscator(ep, sessionKey)
|
||||
seshConfig.Obfuscator = obfuscator
|
||||
seshConfig.Obfuscator, _ = MakeObfuscator(ep, sessionKey)
|
||||
sesh := MakeSession(0, seshConfig)
|
||||
sesh.AddConnection(connutil.Discard())
|
||||
b.ResetTimer()
|
||||
|
|
@ -496,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import (
|
|||
"net"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var ErrBrokenStream = errors.New("broken stream")
|
||||
|
|
@ -23,9 +24,8 @@ type Stream struct {
|
|||
|
||||
session *Session
|
||||
|
||||
allocIdempot sync.Once
|
||||
// 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
|
||||
|
||||
writingM sync.Mutex
|
||||
|
|
@ -40,7 +40,7 @@ type Stream struct {
|
|||
// 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.
|
||||
// This is not used in unordered connection mode
|
||||
assignedConnId uint32
|
||||
assignedConn net.Conn
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
func (s *Stream) recvFrame(frame *Frame) error {
|
||||
toBeClosed, err := s.getRecvBuf().Write(frame)
|
||||
toBeClosed, err := s.recvBuf.Write(frame)
|
||||
if toBeClosed {
|
||||
err = s.passiveClose()
|
||||
if errors.Is(err, errRepeatStreamClosing) {
|
||||
|
|
@ -93,7 +88,7 @@ func (s *Stream) Read(buf []byte) (n int, err error) {
|
|||
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)
|
||||
if err == io.EOF {
|
||||
return n, ErrBrokenStream
|
||||
|
|
@ -101,17 +96,6 @@ func (s *Stream) Read(buf []byte) (n int, err error) {
|
|||
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 {
|
||||
cipherTextLen, err := s.session.obfuscate(&s.writingFrame, buf, payloadOffsetInBuf)
|
||||
s.writingFrame.Seq++
|
||||
|
|
@ -119,7 +103,7 @@ func (s *Stream) obfuscateAndSend(buf []byte, payloadOffsetInBuf int) error {
|
|||
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 == errBrokenSwitchboard {
|
||||
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) 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.getRecvBuf().SetReadDeadline(t); return nil }
|
||||
func (s *Stream) SetReadDeadline(t time.Time) error { s.recvBuf.SetReadDeadline(t); return nil }
|
||||
func (s *Stream) SetReadFromTimeout(d time.Duration) { s.readFromTimeout = d }
|
||||
|
||||
var errNotImplemented = errors.New("Not implemented")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ package multiplex
|
|||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -82,6 +81,8 @@ func (sb *streamBuffer) Write(f *Frame) (toBeClosed bool, err error) {
|
|||
}
|
||||
|
||||
saved := *f
|
||||
saved.Payload = make([]byte, len(f.Payload))
|
||||
copy(saved.Payload, f.Payload)
|
||||
heap.Push(&sb.sh, &saved)
|
||||
// Keep popping from the heap until empty or to the point that the wanted seq was not received
|
||||
for len(sb.sh) > 0 && sb.sh[0].Seq == sb.nextRecvSeq {
|
||||
|
|
@ -100,10 +101,6 @@ func (sb *streamBuffer) Read(buf []byte) (int, error) {
|
|||
return sb.buf.Read(buf)
|
||||
}
|
||||
|
||||
func (sb *streamBuffer) WriteTo(w io.Writer) (int64, error) {
|
||||
return sb.buf.WriteTo(w)
|
||||
}
|
||||
|
||||
func (sb *streamBuffer) Close() error {
|
||||
sb.recvM.Lock()
|
||||
defer sb.recvM.Unlock()
|
||||
|
|
@ -112,4 +109,3 @@ func (sb *streamBuffer) Close() error {
|
|||
}
|
||||
|
||||
func (sb *streamBuffer) SetReadDeadline(t time.Time) { sb.buf.SetReadDeadline(t) }
|
||||
func (sb *streamBuffer) SetWriteToTimeout(d time.Duration) { sb.buf.SetWriteToTimeout(d) }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package multiplex
|
|||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
//"log"
|
||||
"sort"
|
||||
"testing"
|
||||
|
|
|
|||
|
|
@ -58,43 +58,6 @@ func (p *streamBufferedPipe) Read(target []byte) (int, error) {
|
|||
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) {
|
||||
p.rwCond.L.Lock()
|
||||
defer p.rwCond.L.Unlock()
|
||||
|
|
@ -131,14 +94,6 @@ func (p *streamBufferedPipe) SetReadDeadline(t time.Time) {
|
|||
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) {
|
||||
if p.timeoutTimer != nil {
|
||||
p.timeoutTimer.Stop()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
package multiplex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const readBlockTime = 500 * time.Millisecond
|
||||
|
|
@ -13,49 +14,15 @@ func TestPipeRW(t *testing.T) {
|
|||
pipe := NewStreamBufferedPipe()
|
||||
b := []byte{0x01, 0x02, 0x03}
|
||||
n, err := pipe.Write(b)
|
||||
if n != len(b) {
|
||||
t.Error(
|
||||
"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
|
||||
}
|
||||
assert.NoError(t, err, "simple write")
|
||||
assert.Equal(t, len(b), n, "number of bytes written")
|
||||
|
||||
b2 := make([]byte, len(b))
|
||||
n, err = pipe.Read(b2)
|
||||
if n != len(b) {
|
||||
t.Error(
|
||||
"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.NoError(t, err, "simple read")
|
||||
assert.Equal(t, len(b), n, "number of bytes read")
|
||||
|
||||
assert.Equal(t, b, b2)
|
||||
}
|
||||
|
||||
func TestReadBlock(t *testing.T) {
|
||||
|
|
@ -67,30 +34,10 @@ func TestReadBlock(t *testing.T) {
|
|||
}()
|
||||
b2 := make([]byte, len(b))
|
||||
n, err := pipe.Read(b2)
|
||||
if n != len(b) {
|
||||
t.Error(
|
||||
"For", "number of bytes read after block",
|
||||
"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
|
||||
}
|
||||
assert.NoError(t, err, "blocked read")
|
||||
assert.Equal(t, len(b), n, "number of bytes read after block")
|
||||
|
||||
assert.Equal(t, b, b2)
|
||||
}
|
||||
|
||||
func TestPartialRead(t *testing.T) {
|
||||
|
|
@ -99,54 +46,17 @@ func TestPartialRead(t *testing.T) {
|
|||
pipe.Write(b)
|
||||
b1 := make([]byte, 1)
|
||||
n, err := pipe.Read(b1)
|
||||
if n != len(b1) {
|
||||
t.Error(
|
||||
"For", "number of bytes in partial read of 1",
|
||||
"expecting", len(b1),
|
||||
"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],
|
||||
)
|
||||
}
|
||||
assert.NoError(t, err, "partial read of 1")
|
||||
assert.Equal(t, len(b1), n, "number of bytes in partial read of 1")
|
||||
|
||||
assert.Equal(t, b[0], b1[0])
|
||||
|
||||
b2 := make([]byte, 2)
|
||||
n, err = pipe.Read(b2)
|
||||
if n != len(b2) {
|
||||
t.Error(
|
||||
"For", "number of bytes in partial read of 2",
|
||||
"expecting", len(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
|
||||
}
|
||||
assert.NoError(t, err, "partial read of 2")
|
||||
assert.Equal(t, len(b2), n, "number of bytes in partial read of 2")
|
||||
|
||||
assert.Equal(t, b[1:], b2)
|
||||
}
|
||||
|
||||
func TestReadAfterClose(t *testing.T) {
|
||||
|
|
@ -156,29 +66,10 @@ func TestReadAfterClose(t *testing.T) {
|
|||
b2 := make([]byte, len(b))
|
||||
pipe.Close()
|
||||
n, err := pipe.Read(b2)
|
||||
if n != len(b) {
|
||||
t.Error(
|
||||
"For", "number of bytes read",
|
||||
"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
|
||||
}
|
||||
assert.NoError(t, err, "simple read")
|
||||
assert.Equal(t, len(b), n, "number of bytes read")
|
||||
|
||||
assert.Equal(t, b, b2)
|
||||
}
|
||||
|
||||
func BenchmarkBufferedPipe_RW(b *testing.B) {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ package multiplex
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cbeuw/connutil"
|
||||
)
|
||||
|
||||
|
|
@ -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) {
|
||||
seshes := map[string]*Session{
|
||||
"ordered": setupSesh(false, emptyKey, EncryptionMethodPlain),
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@ package multiplex
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type switchboardStrategy int
|
||||
|
||||
const (
|
||||
FIXED_CONN_MAPPING switchboardStrategy = iota
|
||||
UNIFORM_SPREAD
|
||||
fixedConnMapping switchboardStrategy = iota
|
||||
uniformSpread
|
||||
)
|
||||
|
||||
// switchboard represents the connection pool. It is responsible for managing
|
||||
|
|
@ -28,30 +30,22 @@ type switchboard struct {
|
|||
valve Valve
|
||||
strategy switchboardStrategy
|
||||
|
||||
// map of connId to net.Conn
|
||||
conns sync.Map
|
||||
numConns uint32
|
||||
nextConnId uint32
|
||||
connsCount uint32
|
||||
randPool sync.Pool
|
||||
|
||||
broken uint32
|
||||
}
|
||||
|
||||
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{
|
||||
session: sesh,
|
||||
strategy: strategy,
|
||||
strategy: uniformSpread,
|
||||
valve: sesh.Valve,
|
||||
nextConnId: 1,
|
||||
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
|
||||
|
|
@ -59,88 +53,81 @@ func makeSwitchboard(sesh *Session) *switchboard {
|
|||
|
||||
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) {
|
||||
connId := atomic.AddUint32(&sb.nextConnId, 1) - 1
|
||||
atomic.AddUint32(&sb.numConns, 1)
|
||||
connId := atomic.AddUint32(&sb.connsCount, 1) - 1
|
||||
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
|
||||
func (sb *switchboard) send(data []byte, connId *uint32) (n int, err error) {
|
||||
// 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, assignedConn *net.Conn) (n int, err error) {
|
||||
sb.valve.txWait(len(data))
|
||||
if atomic.LoadUint32(&sb.broken) == 1 || sb.connsCount() == 0 {
|
||||
if atomic.LoadUint32(&sb.broken) == 1 {
|
||||
return 0, errBrokenSwitchboard
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
switch sb.strategy {
|
||||
case UNIFORM_SPREAD:
|
||||
_, conn, err = sb.pickRandConn()
|
||||
case uniformSpread:
|
||||
conn, err = sb.pickRandConn()
|
||||
if err != nil {
|
||||
return 0, errBrokenSwitchboard
|
||||
}
|
||||
case FIXED_CONN_MAPPING:
|
||||
connI, ok := sb.conns.Load(*connId)
|
||||
if ok {
|
||||
conn = connI.(net.Conn)
|
||||
} else {
|
||||
var newConnId uint32
|
||||
newConnId, conn, err = sb.pickRandConn()
|
||||
n, err = conn.Write(data)
|
||||
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:
|
||||
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))
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// returns a random connId
|
||||
func (sb *switchboard) pickRandConn() (uint32, net.Conn, error) {
|
||||
connCount := sb.connsCount()
|
||||
if atomic.LoadUint32(&sb.broken) == 1 || connCount == 0 {
|
||||
return 0, nil, errBrokenSwitchboard
|
||||
// returns a random conn. This function can be called concurrently.
|
||||
func (sb *switchboard) pickRandConn() (net.Conn, error) {
|
||||
if atomic.LoadUint32(&sb.broken) == 1 {
|
||||
return 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)
|
||||
r := randReader.Intn(connCount)
|
||||
connId := randReader.Uint32N(connsCount)
|
||||
sb.randPool.Put(randReader)
|
||||
var c int
|
||||
sb.conns.Range(func(connIdI, connI interface{}) bool {
|
||||
if r == c {
|
||||
id = connIdI.(uint32)
|
||||
conn = connI.(net.Conn)
|
||||
return false
|
||||
|
||||
ret, ok := sb.conns.Load(connId)
|
||||
if !ok {
|
||||
log.Errorf("failed to get conn %d", connId)
|
||||
return nil, errBrokenSwitchboard
|
||||
}
|
||||
c++
|
||||
return true
|
||||
})
|
||||
// if len(sb.conns) is 0
|
||||
if conn == nil {
|
||||
return 0, nil, errBrokenSwitchboard
|
||||
}
|
||||
return id, conn, nil
|
||||
return ret.(net.Conn), nil
|
||||
}
|
||||
|
||||
// actively triggered by session.Close()
|
||||
|
|
@ -148,26 +135,24 @@ func (sb *switchboard) closeAll() {
|
|||
if !atomic.CompareAndSwapUint32(&sb.broken, 0, 1) {
|
||||
return
|
||||
}
|
||||
sb.conns.Range(func(key, connI interface{}) bool {
|
||||
conn := connI.(net.Conn)
|
||||
conn.Close()
|
||||
sb.conns.Delete(key)
|
||||
atomic.StoreUint32(&sb.connsCount, 0)
|
||||
sb.conns.Range(func(_, conn interface{}) bool {
|
||||
conn.(net.Conn).Close()
|
||||
sb.conns.Delete(conn)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 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()
|
||||
buf := make([]byte, sb.session.ConnReceiveBufferSize)
|
||||
buf := make([]byte, sb.session.connReceiveBufferSize)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
sb.valve.rxWait(n)
|
||||
sb.valve.AddRx(int64(n))
|
||||
if err != nil {
|
||||
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.passiveClose()
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package multiplex
|
||||
|
||||
import (
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSwitchboard_Send(t *testing.T) {
|
||||
|
|
@ -14,14 +16,14 @@ func TestSwitchboard_Send(t *testing.T) {
|
|||
sesh := MakeSession(0, seshConfig)
|
||||
hole0 := connutil.Discard()
|
||||
sesh.sb.addConn(hole0)
|
||||
connId, _, err := sesh.sb.pickRandConn()
|
||||
conn, err := sesh.sb.pickRandConn()
|
||||
if err != nil {
|
||||
t.Error("failed to get a random conn", err)
|
||||
return
|
||||
}
|
||||
data := make([]byte, 1000)
|
||||
rand.Read(data)
|
||||
_, err = sesh.sb.send(data, &connId)
|
||||
_, err = sesh.sb.send(data, &conn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
|
|
@ -29,23 +31,23 @@ func TestSwitchboard_Send(t *testing.T) {
|
|||
|
||||
hole1 := connutil.Discard()
|
||||
sesh.sb.addConn(hole1)
|
||||
connId, _, err = sesh.sb.pickRandConn()
|
||||
conn, err = sesh.sb.pickRandConn()
|
||||
if err != nil {
|
||||
t.Error("failed to get a random conn", err)
|
||||
return
|
||||
}
|
||||
_, err = sesh.sb.send(data, &connId)
|
||||
_, err = sesh.sb.send(data, &conn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
connId, _, err = sesh.sb.pickRandConn()
|
||||
conn, err = sesh.sb.pickRandConn()
|
||||
if err != nil {
|
||||
t.Error("failed to get a random conn", err)
|
||||
return
|
||||
}
|
||||
_, err = sesh.sb.send(data, &connId)
|
||||
_, err = sesh.sb.send(data, &conn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
|
|
@ -71,7 +73,7 @@ func BenchmarkSwitchboard_Send(b *testing.B) {
|
|||
seshConfig := SessionConfig{}
|
||||
sesh := MakeSession(0, seshConfig)
|
||||
sesh.sb.addConn(hole)
|
||||
connId, _, err := sesh.sb.pickRandConn()
|
||||
conn, err := sesh.sb.pickRandConn()
|
||||
if err != nil {
|
||||
b.Error("failed to get a random conn", err)
|
||||
return
|
||||
|
|
@ -81,7 +83,7 @@ func BenchmarkSwitchboard_Send(b *testing.B) {
|
|||
b.SetBytes(int64(len(data)))
|
||||
b.ResetTimer()
|
||||
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)
|
||||
hole := connutil.Discard()
|
||||
sesh.sb.addConn(hole)
|
||||
connId, _, err := sesh.sb.pickRandConn()
|
||||
conn, err := sesh.sb.pickRandConn()
|
||||
if err != nil {
|
||||
t.Error("failed to get a random conn", err)
|
||||
return
|
||||
|
|
@ -100,10 +102,10 @@ func TestSwitchboard_TxCredit(t *testing.T) {
|
|||
data := make([]byte, 1000)
|
||||
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.strategy = FIXED_CONN_MAPPING
|
||||
n, err := sesh.sb.send(data[:10], &connId)
|
||||
sesh.sb.strategy = fixedConnMapping
|
||||
n, err := sesh.sb.send(data[:10], &conn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
|
|
@ -116,10 +118,10 @@ func TestSwitchboard_TxCredit(t *testing.T) {
|
|||
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.strategy = UNIFORM_SPREAD
|
||||
n, err := sesh.sb.send(data[:10], &connId)
|
||||
sesh.sb.strategy = uniformSpread
|
||||
n, err := sesh.sb.send(data[:10], &conn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
|
|
@ -173,13 +175,13 @@ func TestSwitchboard_ConnsCount(t *testing.T) {
|
|||
}
|
||||
wg.Wait()
|
||||
|
||||
if sesh.sb.connsCount() != 1000 {
|
||||
if atomic.LoadUint32(&sesh.sb.connsCount) != 1000 {
|
||||
t.Error("connsCount incorrect")
|
||||
}
|
||||
|
||||
sesh.sb.closeAll()
|
||||
|
||||
assert.Eventuallyf(t, func() bool {
|
||||
return sesh.sb.connsCount() == 0
|
||||
}, time.Second, 10*time.Millisecond, "connsCount incorrect: %v", sesh.sb.connsCount())
|
||||
return atomic.LoadUint32(&sesh.sb.connsCount) == 0
|
||||
}, time.Second, 10*time.Millisecond, "connsCount incorrect: %v", atomic.LoadUint32(&sesh.sb.connsCount))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import (
|
|||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
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
|
||||
// we can use sessionKey as a seed here to ensure consistency
|
||||
possibleCertLengths := []int{42, 27, 68, 59, 36, 44, 46}
|
||||
rand.Seed(int64(sessionKey[0]))
|
||||
cert := make([]byte, possibleCertLengths[rand.Intn(len(possibleCertLengths))])
|
||||
cert := make([]byte, possibleCertLengths[common.RandInt(len(possibleCertLengths))])
|
||||
common.RandRead(randSource, cert)
|
||||
|
||||
var nonce [12]byte
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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 {
|
||||
var serverHello [11][]byte
|
||||
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[3] = append(nonce[0:12], encryptedSessionKeyWithTag[0:20]...) // random 32 bytes
|
||||
serverHello[4] = []byte{0x20} // session id length 32
|
||||
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[8] = []byte{0x00, 0x2e} // extensions length 46
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
"sync"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ package server
|
|||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"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 {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import (
|
|||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
|
||||
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 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
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
if _, ok := sta.ProxyBook[info.ProxyMethod]; !ok {
|
||||
err = ErrBadProxyMethod
|
||||
return
|
||||
}
|
||||
info.Transport = transport
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import (
|
|||
"crypto"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
)
|
||||
|
||||
func TestDecryptClientInfo(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -6,19 +6,22 @@ import (
|
|||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var b64 = base64.StdEncoding.EncodeToString
|
||||
|
||||
const firstPacketSize = 3000
|
||||
|
||||
func Serve(l net.Listener, sta *State) {
|
||||
waitDur := [10]time.Duration{
|
||||
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) {
|
||||
var err error
|
||||
buf := make([]byte, 1500)
|
||||
buf := make([]byte, firstPacketSize)
|
||||
|
||||
i, transport, redirOnErr, err := readFirstPacket(conn, buf, 15*time.Second)
|
||||
data := buf[:i]
|
||||
|
|
@ -213,6 +216,18 @@ func dispatchConnection(conn net.Conn, sta *State) {
|
|||
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
|
||||
if sta.IsBypass(ci.UID) {
|
||||
user, err = sta.Panel.GetBypassUser(ci.UID)
|
||||
|
|
@ -236,7 +251,7 @@ func dispatchConnection(conn net.Conn, sta *State) {
|
|||
return
|
||||
}
|
||||
|
||||
preparedConn, err := finishHandshake(conn, sesh.SessionKey, sta.WorldState.Rand)
|
||||
preparedConn, err := finishHandshake(conn, sesh.GetSessionKey(), sta.WorldState.Rand)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package server
|
|||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type rfpReturnValue struct {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/connutil"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/connutil"
|
||||
)
|
||||
|
||||
type rfpReturnValue_fuzz struct {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
)
|
||||
|
||||
type RawConfig struct {
|
||||
|
|
|
|||
|
|
@ -43,13 +43,22 @@ func TestParseRedirAddr(t *testing.T) {
|
|||
t.Errorf("parsing %v error: %v", domainNoPort, err)
|
||||
return
|
||||
}
|
||||
expHost, err := net.ResolveIPAddr("ip", "example.com")
|
||||
|
||||
expIPs, err := net.LookupIP("example.com")
|
||||
if err != nil {
|
||||
t.Errorf("tester error: cannot resolve example.com: %v", err)
|
||||
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 != "" {
|
||||
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)
|
||||
return
|
||||
}
|
||||
expHost, err := net.ResolveIPAddr("ip", "example.com")
|
||||
|
||||
expIPs, err := net.LookupIP("example.com")
|
||||
if err != nil {
|
||||
t.Errorf("tester error: cannot resolve example.com: %v", err)
|
||||
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" {
|
||||
t.Errorf("wrong port: expected %v, got %v", "80", port)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ swagger: '2.0'
|
|||
info:
|
||||
description: |
|
||||
This is the API of Cloak server
|
||||
version: 1.0.0
|
||||
version: 0.0.2
|
||||
title: Cloak Server
|
||||
contact:
|
||||
email: cbeuw.andy@gmail.com
|
||||
|
|
@ -12,8 +12,6 @@ info:
|
|||
# host: petstore.swagger.io
|
||||
# basePath: /v2
|
||||
tags:
|
||||
- name: admin
|
||||
description: Endpoints used by the host administrators
|
||||
- name: users
|
||||
description: Operations related to user controls by admin
|
||||
# schemes:
|
||||
|
|
@ -22,7 +20,6 @@ paths:
|
|||
/admin/users:
|
||||
get:
|
||||
tags:
|
||||
- admin
|
||||
- users
|
||||
summary: Show all users
|
||||
description: Returns an array of all UserInfo
|
||||
|
|
@ -41,7 +38,6 @@ paths:
|
|||
/admin/users/{UID}:
|
||||
get:
|
||||
tags:
|
||||
- admin
|
||||
- users
|
||||
summary: Show userinfo by UID
|
||||
description: Returns a UserInfo object
|
||||
|
|
@ -68,7 +64,6 @@ paths:
|
|||
description: internal error
|
||||
post:
|
||||
tags:
|
||||
- admin
|
||||
- users
|
||||
summary: Updates the userinfo of the specified user, if the user does not exist, then a new user is created
|
||||
operationId: writeUserInfo
|
||||
|
|
@ -100,7 +95,6 @@ paths:
|
|||
description: internal error
|
||||
delete:
|
||||
tags:
|
||||
- admin
|
||||
- users
|
||||
summary: Deletes a user
|
||||
operationId: deleteUser
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import (
|
|||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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) {
|
||||
req, err := http.NewRequest("POST", "/admin/users/", bytes.NewBuffer(marshalled))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package usermanager
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
|
@ -127,6 +128,7 @@ func (manager *localManager) UploadStatus(uploads []StatusUpdate) ([]StatusRespo
|
|||
"User no longer exists",
|
||||
}
|
||||
responses = append(responses, resp)
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
var uinfo UserInfo
|
||||
uinfo.UID = UID
|
||||
uinfo.SessionsCap = int32(u32(bucket.Get([]byte("SessionsCap"))))
|
||||
uinfo.UpRate = int64(u64(bucket.Get([]byte("UpRate"))))
|
||||
uinfo.DownRate = int64(u64(bucket.Get([]byte("DownRate"))))
|
||||
uinfo.UpCredit = int64(u64(bucket.Get([]byte("UpCredit"))))
|
||||
uinfo.DownCredit = int64(u64(bucket.Get([]byte("DownCredit"))))
|
||||
uinfo.ExpiryTime = int64(u64(bucket.Get([]byte("ExpiryTime"))))
|
||||
uinfo.SessionsCap = JustInt32(int32(u32(bucket.Get([]byte("SessionsCap")))))
|
||||
uinfo.UpRate = JustInt64(int64(u64(bucket.Get([]byte("UpRate")))))
|
||||
uinfo.DownRate = JustInt64(int64(u64(bucket.Get([]byte("DownRate")))))
|
||||
uinfo.UpCredit = JustInt64(int64(u64(bucket.Get([]byte("UpCredit")))))
|
||||
uinfo.DownCredit = JustInt64(int64(u64(bucket.Get([]byte("DownCredit")))))
|
||||
uinfo.ExpiryTime = JustInt64(int64(u64(bucket.Get([]byte("ExpiryTime")))))
|
||||
infos = append(infos, uinfo)
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
if infos == nil {
|
||||
infos = []UserInfo{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -200,41 +205,53 @@ func (manager *localManager) GetUserInfo(UID []byte) (uinfo UserInfo, err error)
|
|||
return ErrUserNotFound
|
||||
}
|
||||
uinfo.UID = UID
|
||||
uinfo.SessionsCap = int32(u32(bucket.Get([]byte("SessionsCap"))))
|
||||
uinfo.UpRate = int64(u64(bucket.Get([]byte("UpRate"))))
|
||||
uinfo.DownRate = int64(u64(bucket.Get([]byte("DownRate"))))
|
||||
uinfo.UpCredit = int64(u64(bucket.Get([]byte("UpCredit"))))
|
||||
uinfo.DownCredit = int64(u64(bucket.Get([]byte("DownCredit"))))
|
||||
uinfo.ExpiryTime = int64(u64(bucket.Get([]byte("ExpiryTime"))))
|
||||
uinfo.SessionsCap = JustInt32(int32(u32(bucket.Get([]byte("SessionsCap")))))
|
||||
uinfo.UpRate = JustInt64(int64(u64(bucket.Get([]byte("UpRate")))))
|
||||
uinfo.DownRate = JustInt64(int64(u64(bucket.Get([]byte("DownRate")))))
|
||||
uinfo.UpCredit = JustInt64(int64(u64(bucket.Get([]byte("UpCredit")))))
|
||||
uinfo.DownCredit = JustInt64(int64(u64(bucket.Get([]byte("DownCredit")))))
|
||||
uinfo.ExpiryTime = JustInt64(int64(u64(bucket.Get([]byte("ExpiryTime")))))
|
||||
return nil
|
||||
})
|
||||
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 {
|
||||
bucket, err := tx.CreateBucketIfNotExists(uinfo.UID)
|
||||
bucket, err := tx.CreateBucketIfNotExists(u.UID)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 nil
|
||||
})
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package usermanager
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
|
|
@ -11,18 +10,21 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
"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 mockWorldState = common.WorldOfTime(time.Unix(1, 0))
|
||||
var mockUserInfo = UserInfo{
|
||||
UID: mockUID,
|
||||
SessionsCap: 0,
|
||||
UpRate: 0,
|
||||
DownRate: 0,
|
||||
UpCredit: 0,
|
||||
DownCredit: 0,
|
||||
ExpiryTime: 100,
|
||||
SessionsCap: JustInt32(10),
|
||||
UpRate: JustInt64(100),
|
||||
DownRate: JustInt64(1000),
|
||||
UpCredit: JustInt64(10000),
|
||||
DownCredit: JustInt64(100000),
|
||||
ExpiryTime: JustInt64(1000000),
|
||||
}
|
||||
|
||||
func makeManager(t *testing.T) (mgr *localManager, cleaner func()) {
|
||||
|
|
@ -43,6 +45,23 @@ func TestLocalManager_WriteUserInfo(t *testing.T) {
|
|||
if err != nil {
|
||||
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) {
|
||||
|
|
@ -63,7 +82,7 @@ func TestLocalManager_GetUserInfo(t *testing.T) {
|
|||
t.Run("update a field", func(t *testing.T) {
|
||||
_ = mgr.WriteUserInfo(mockUserInfo)
|
||||
updatedUserInfo := mockUserInfo
|
||||
updatedUserInfo.SessionsCap = mockUserInfo.SessionsCap + 1
|
||||
updatedUserInfo.SessionsCap = JustInt32(*mockUserInfo.SessionsCap + 1)
|
||||
|
||||
err := mgr.WriteUserInfo(updatedUserInfo)
|
||||
if err != nil {
|
||||
|
|
@ -103,15 +122,7 @@ func TestLocalManager_DeleteUser(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
var validUserInfo = UserInfo{
|
||||
UID: mockUID,
|
||||
SessionsCap: 10,
|
||||
UpRate: 100,
|
||||
DownRate: 1000,
|
||||
UpCredit: 10000,
|
||||
DownCredit: 100000,
|
||||
ExpiryTime: 1000000,
|
||||
}
|
||||
var validUserInfo = mockUserInfo
|
||||
|
||||
func TestLocalManager_AuthenticateUser(t *testing.T) {
|
||||
var tmpDB, _ = ioutil.TempFile("", "ck_user_info")
|
||||
|
|
@ -128,7 +139,7 @@ func TestLocalManager_AuthenticateUser(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
|
||||
if upRate != validUserInfo.UpRate || downRate != validUserInfo.DownRate {
|
||||
if upRate != *validUserInfo.UpRate || downRate != *validUserInfo.DownRate {
|
||||
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) {
|
||||
expiredUserInfo := validUserInfo
|
||||
expiredUserInfo.ExpiryTime = mockWorldState.Now().Add(-10 * time.Second).Unix()
|
||||
expiredUserInfo.ExpiryTime = JustInt64(mockWorldState.Now().Add(-10 * time.Second).Unix())
|
||||
|
||||
_ = mgr.WriteUserInfo(expiredUserInfo)
|
||||
|
||||
|
|
@ -154,7 +165,7 @@ func TestLocalManager_AuthenticateUser(t *testing.T) {
|
|||
|
||||
t.Run("no credit", func(t *testing.T) {
|
||||
creditlessUserInfo := validUserInfo
|
||||
creditlessUserInfo.UpCredit, creditlessUserInfo.DownCredit = -1, -1
|
||||
creditlessUserInfo.UpCredit, creditlessUserInfo.DownCredit = JustInt64(-1), JustInt64(-1)
|
||||
|
||||
_ = mgr.WriteUserInfo(creditlessUserInfo)
|
||||
|
||||
|
|
@ -186,7 +197,7 @@ func TestLocalManager_AuthoriseNewSession(t *testing.T) {
|
|||
|
||||
t.Run("expired user", func(t *testing.T) {
|
||||
expiredUserInfo := validUserInfo
|
||||
expiredUserInfo.ExpiryTime = mockWorldState.Now().Add(-10 * time.Second).Unix()
|
||||
expiredUserInfo.ExpiryTime = JustInt64(mockWorldState.Now().Add(-10 * time.Second).Unix())
|
||||
|
||||
_ = mgr.WriteUserInfo(expiredUserInfo)
|
||||
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) {
|
||||
_ = 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 {
|
||||
t.Error("session cap not reached")
|
||||
}
|
||||
|
|
@ -230,10 +241,10 @@ func TestLocalManager_UploadStatus(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
|
||||
if updatedUserInfo.UpCredit != validUserInfo.UpCredit-update.UpUsage {
|
||||
if *updatedUserInfo.UpCredit != *validUserInfo.UpCredit-update.UpUsage {
|
||||
t.Error("up usage incorrect")
|
||||
}
|
||||
if updatedUserInfo.DownCredit != validUserInfo.DownCredit-update.DownUsage {
|
||||
if *updatedUserInfo.DownCredit != *validUserInfo.DownCredit-update.DownUsage {
|
||||
t.Error("down usage incorrect")
|
||||
}
|
||||
})
|
||||
|
|
@ -249,7 +260,7 @@ func TestLocalManager_UploadStatus(t *testing.T) {
|
|||
UID: validUserInfo.UID,
|
||||
Active: true,
|
||||
NumSession: 1,
|
||||
UpUsage: validUserInfo.UpCredit + 100,
|
||||
UpUsage: *validUserInfo.UpCredit + 100,
|
||||
DownUsage: 0,
|
||||
Timestamp: mockWorldState.Now().Unix(),
|
||||
},
|
||||
|
|
@ -261,19 +272,19 @@ func TestLocalManager_UploadStatus(t *testing.T) {
|
|||
Active: true,
|
||||
NumSession: 1,
|
||||
UpUsage: 0,
|
||||
DownUsage: validUserInfo.DownCredit + 100,
|
||||
DownUsage: *validUserInfo.DownCredit + 100,
|
||||
Timestamp: mockWorldState.Now().Unix(),
|
||||
},
|
||||
},
|
||||
{"expired",
|
||||
UserInfo{
|
||||
UID: mockUID,
|
||||
SessionsCap: 10,
|
||||
UpRate: 0,
|
||||
DownRate: 0,
|
||||
UpCredit: 0,
|
||||
DownCredit: 0,
|
||||
ExpiryTime: -1,
|
||||
SessionsCap: JustInt32(10),
|
||||
UpRate: JustInt64(0),
|
||||
DownRate: JustInt64(0),
|
||||
UpCredit: JustInt64(0),
|
||||
DownCredit: JustInt64(0),
|
||||
ExpiryTime: JustInt64(-1),
|
||||
},
|
||||
StatusUpdate{
|
||||
UID: mockUserInfo.UID,
|
||||
|
|
@ -318,12 +329,12 @@ func TestLocalManager_ListAllUsers(t *testing.T) {
|
|||
rand.Read(randUID)
|
||||
newUser := UserInfo{
|
||||
UID: randUID,
|
||||
SessionsCap: rand.Int31(),
|
||||
UpRate: rand.Int63(),
|
||||
DownRate: rand.Int63(),
|
||||
UpCredit: rand.Int63(),
|
||||
DownCredit: rand.Int63(),
|
||||
ExpiryTime: rand.Int63(),
|
||||
SessionsCap: JustInt32(rand.Int31()),
|
||||
UpRate: JustInt64(rand.Int63()),
|
||||
DownRate: JustInt64(rand.Int63()),
|
||||
UpCredit: JustInt64(rand.Int63()),
|
||||
DownCredit: JustInt64(rand.Int63()),
|
||||
ExpiryTime: JustInt64(rand.Int63()),
|
||||
}
|
||||
users = append(users, newUser)
|
||||
wg.Add(1)
|
||||
|
|
|
|||
|
|
@ -14,16 +14,23 @@ type StatusUpdate struct {
|
|||
Timestamp int64
|
||||
}
|
||||
|
||||
type MaybeInt32 *int32
|
||||
type MaybeInt64 *int64
|
||||
|
||||
type UserInfo struct {
|
||||
UID []byte
|
||||
SessionsCap int32
|
||||
UpRate int64
|
||||
DownRate int64
|
||||
UpCredit int64
|
||||
DownCredit int64
|
||||
ExpiryTime int64
|
||||
SessionsCap MaybeInt32
|
||||
UpRate MaybeInt64
|
||||
DownRate MaybeInt64
|
||||
UpCredit MaybeInt64
|
||||
DownCredit MaybeInt64
|
||||
ExpiryTime MaybeInt64
|
||||
}
|
||||
|
||||
func JustInt32(v int32) MaybeInt32 { return &v }
|
||||
|
||||
func JustInt64(v int64) MaybeInt64 { return &v }
|
||||
|
||||
type StatusResponse struct {
|
||||
UID []byte
|
||||
Action int
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ func (v *Voidmanager) UploadStatus(updates []StatusUpdate) ([]StatusResponse, er
|
|||
}
|
||||
|
||||
func (v *Voidmanager) ListAllUsers() ([]UserInfo, error) {
|
||||
return nil, ErrMangerIsVoid
|
||||
return []UserInfo{}, ErrMangerIsVoid
|
||||
}
|
||||
|
||||
func (v *Voidmanager) GetUserInfo(UID []byte) (UserInfo, error) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package usermanager
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var v = &Voidmanager{}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package server
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package server
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/server/usermanager"
|
||||
)
|
||||
|
||||
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 validUserInfo = usermanager.UserInfo{
|
||||
UID: mockUID,
|
||||
SessionsCap: 10,
|
||||
UpRate: 100,
|
||||
DownRate: 1000,
|
||||
UpCredit: 10000,
|
||||
DownCredit: 100000,
|
||||
ExpiryTime: 1000000,
|
||||
SessionsCap: usermanager.JustInt32(10),
|
||||
UpRate: usermanager.JustInt64(100),
|
||||
DownRate: usermanager.JustInt64(1000),
|
||||
UpCredit: usermanager.JustInt64(10000),
|
||||
DownCredit: usermanager.JustInt64(100000),
|
||||
ExpiryTime: usermanager.JustInt64(1000000),
|
||||
}
|
||||
|
||||
func TestUserPanel_GetUser(t *testing.T) {
|
||||
|
|
@ -138,10 +139,10 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
|
|||
}
|
||||
|
||||
updatedUinfo, _ := mgr.GetUserInfo(validUserInfo.UID)
|
||||
if updatedUinfo.DownCredit != validUserInfo.DownCredit-1 {
|
||||
if *updatedUinfo.DownCredit != *validUserInfo.DownCredit-1 {
|
||||
t.Error("down credit incorrect update")
|
||||
}
|
||||
if updatedUinfo.UpCredit != validUserInfo.UpCredit-2 {
|
||||
if *updatedUinfo.UpCredit != *validUserInfo.UpCredit-2 {
|
||||
t.Error("up credit incorrect update")
|
||||
}
|
||||
|
||||
|
|
@ -155,10 +156,10 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
|
|||
}
|
||||
|
||||
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")
|
||||
}
|
||||
if updatedUinfo.UpCredit != validUserInfo.UpCredit-(2+4) {
|
||||
if *updatedUinfo.UpCredit != *validUserInfo.UpCredit-(2+4) {
|
||||
t.Error("up credit incorrect update")
|
||||
}
|
||||
})
|
||||
|
|
@ -170,7 +171,7 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
|
||||
user.valve.AddTx(validUserInfo.DownCredit + 100)
|
||||
user.valve.AddTx(*validUserInfo.DownCredit + 100)
|
||||
panel.updateUsageQueue()
|
||||
err = panel.commitUpdate()
|
||||
if err != nil {
|
||||
|
|
@ -182,7 +183,7 @@ func TestUserPanel_UpdateUsageQueue(t *testing.T) {
|
|||
}
|
||||
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import (
|
|||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/cbeuw/Cloak/internal/ecdh"
|
||||
)
|
||||
|
||||
type WebSocket struct{}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package server
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/gorilla/websocket"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package server
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cbeuw/connutil"
|
||||
"testing"
|
||||
|
||||
"github.com/cbeuw/connutil"
|
||||
)
|
||||
|
||||
func TestFirstBuffedConn_Read(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"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"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
|
@ -18,6 +12,13 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cbeuw/Cloak/internal/client"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
mux "github.com/cbeuw/Cloak/internal/multiplex"
|
||||
"github.com/cbeuw/Cloak/internal/server"
|
||||
"github.com/cbeuw/connutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
@ -120,7 +121,7 @@ var singleplexTCPConfig = client.RawConfig{
|
|||
RemotePort: "9999",
|
||||
LocalHost: "127.0.0.1",
|
||||
LocalPort: "9999",
|
||||
BrowserSig: "chrome",
|
||||
BrowserSig: "safari",
|
||||
}
|
||||
|
||||
func generateClientConfigs(rawConfig client.RawConfig, state common.WorldState) (client.LocalConnConfig, client.RemoteConnConfig, client.AuthInfo) {
|
||||
|
|
@ -222,14 +223,15 @@ func establishSession(lcc client.LocalConnConfig, rcc client.RemoteConnConfig, a
|
|||
|
||||
func runEchoTest(t *testing.T, conns []net.Conn, msgLen int) {
|
||||
var wg sync.WaitGroup
|
||||
testData := make([]byte, msgLen)
|
||||
rand.Read(testData)
|
||||
|
||||
for _, conn := range conns {
|
||||
wg.Add(1)
|
||||
go func(conn net.Conn) {
|
||||
defer wg.Done()
|
||||
|
||||
testData := make([]byte, msgLen)
|
||||
rand.Read(testData)
|
||||
|
||||
// we cannot call t.Fatalf in concurrent contexts
|
||||
n, err := conn.Write(testData)
|
||||
if n != msgLen {
|
||||
|
|
@ -320,7 +322,7 @@ func TestTCPSingleplex(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const echoMsgLen = 16384
|
||||
const echoMsgLen = 1 << 16
|
||||
go serveTCPEcho(proxyFromCkServerL)
|
||||
|
||||
proxyConn1, err := proxyToCkClientD.Dial("", "")
|
||||
|
|
|
|||
12
release.sh
12
release.sh
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
go get github.com/mitchellh/gox
|
||||
set -eu
|
||||
|
||||
go install github.com/mitchellh/gox@latest
|
||||
|
||||
mkdir -p release
|
||||
|
||||
|
|
@ -12,13 +14,13 @@ if [ -z "$v" ]; then
|
|||
fi
|
||||
|
||||
output="{{.Dir}}-{{.OS}}-{{.Arch}}-$v"
|
||||
osarch="!darwin/arm !darwin/arm64 !darwin/386"
|
||||
osarch="!darwin/arm !darwin/386"
|
||||
|
||||
echo "Compiling:"
|
||||
|
||||
os="windows linux darwin"
|
||||
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 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}"
|
||||
|
|
@ -27,7 +29,9 @@ popd
|
|||
|
||||
os="linux"
|
||||
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"
|
||||
mv ck-server-* ../../release
|
||||
popd
|
||||
|
||||
sha256sum release/*
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"packagePatterns": ["*"],
|
||||
"excludePackagePatterns": ["utls"],
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue