From b8a9c7ad1a172edf2ae30ff7d2916687b505ef9c Mon Sep 17 00:00:00 2001 From: howmp Date: Thu, 10 Oct 2024 11:08:23 +0800 Subject: [PATCH] init --- .gitignore | 30 +++ LICENSE | 21 ++ README-REALITY.md | 70 +++++++ README.md | 79 ++++++++ build.sh | 19 ++ client.go | 255 +++++++++++++++++++++++ client_test.go | 66 ++++++ cmd/common.go | 8 + cmd/grsc/main.go | 72 +++++++ cmd/grss/gen.go | 159 +++++++++++++++ cmd/grss/main.go | 30 +++ cmd/grss/serv.go | 163 +++++++++++++++ cmd/grsu/main.go | 112 ++++++++++ example/testcipher/main.go | 142 +++++++++++++ example/testclient/main.go | 60 ++++++ example/testserver/main.go | 43 ++++ go.mod | 23 +++ go.sum | 39 ++++ server.go | 405 +++++++++++++++++++++++++++++++++++++ utils.go | 254 +++++++++++++++++++++++ 20 files changed, 2050 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README-REALITY.md create mode 100644 README.md create mode 100755 build.sh create mode 100644 client.go create mode 100644 client_test.go create mode 100644 cmd/common.go create mode 100644 cmd/grsc/main.go create mode 100644 cmd/grss/gen.go create mode 100644 cmd/grss/main.go create mode 100644 cmd/grss/serv.go create mode 100644 cmd/grsu/main.go create mode 100644 example/testcipher/main.go create mode 100644 example/testclient/main.go create mode 100644 example/testserver/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 server.go create mode 100644 utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1e4156 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env +config.json +dist/ +cmd/grss/client/grs* +cmd/grss/files.go +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9535ae1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 [fullname] + +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. diff --git a/README-REALITY.md b/README-REALITY.md new file mode 100644 index 0000000..23cf109 --- /dev/null +++ b/README-REALITY.md @@ -0,0 +1,70 @@ +## reality + + + +reality是安全传输层的实现,其和TLS类似都实现了安全传输,除此之外还进行TLS指纹伪装 + +简单来说就是: + +1. 确定一个伪装服务器目标,比如https://example.com +1. 当普通客户端来访问reality服务端时,将其代理到example.com +1. 当特殊客户端来访问reality服务端时,进行特定处理流程 + +### reality原理 + +具体来说就是在客户端与伪装服务器进行TLS握手的同时,也进行了私有握手 + +首先reality服务端和特殊客户端预先共享一对公私密钥(x25519) + +私有握手关键步骤如下: + +1. 特殊客户端在Client Hello中 + 1. 生成临时公私密钥对(x25519) + 1. Client Hello中将Extension的key_share修改为临时公钥 + 1. 通过临时私钥与预先共享的公钥,以及hkdf算法生成authkey + 1. 通过authkey对版本号、时间戳等信息加密,并替换Client Hello中的Session ID字段 +1. reality服务端收到Client Hello后 + 1. 通过预先共享的私钥和Client Hello中的临时公钥,以及hkdf算法生成authkey + 1. 通过authkey解密Session ID字段,并验证时间戳、版本号信息 + 1. 验证成功则生成一个临时可信证书(ed25519) + 1. 验证失败则代理到伪装服务器 +1. 特殊客户端在收到reality服务端证书后 + 1. 通过hmac算法和authkey计算证书签名,与收到的证书签名对比 + 1. 若签名一致,进行特定处理流程 + 1. 若签名不一致 + 1. 但签名是example.com的真证书,则进入爬虫模式 + 1. 否则发送TLS alert + + + +### reality的特点和限制 + +特点: + +1. 完美模拟了伪装服务器的TLS指纹 +1. 特殊客户端巧妙的利用TLS1.3的key_share和Session ID字段进行私有握手 + 1. 这两字段原本都是随机的,即使替换也没有特征 +1. 不需要域名,也不需要证书 + +限制: + +只能使用TLS1.3,且必须使用x25519 + +1. key_share是TLS1.3新增内容 +1. reality服务端返回的临时证书本质上是有特征的,但TLS1.3中Certificate包是加密的,也就规避了这一问题 +1. 如果伪装服务器目标不使用x25519,则私有握手无法成功 + + +## 与原版的reality的区别 + +1. 使用两组预共享公私钥,分别用于密钥交换/验签,验签使用额外一次通信进行 +2. 模仿站必须是tls1.2,且最好使用aead的套件 + 1. TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 + 1. TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 + 1. TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + 1. TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + 1. TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + 1. TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + 1. TLS_RSA_WITH_AES_128_GCM_SHA256 + 1. TLS_RSA_WITH_AES_256_GCM_SHA384 +3. 服务端代码实现更简单,不需要修改tls库,用读写过滤的方式来判断是否已经握手完成 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..07899df --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# grs + +1. grss(Golang Reverse SOCKS5 Server) 服务端,需要有公网IP的机器上 +1. grsc(Golang Reverse SOCKS5 Client) 客户端,需要运行于想要穿透的内网中机器上 +1. grsu(Golang Reverse SOCKS5 User) 用户端,需要运行于用户机器上,提供socks5服务 + + +grs是一个反向socks5代理,其中grss和grsc和grsu是通过REALITY协议通信 + +关于REALITY协议: [README-REALITY.md](./README-REALITY.md) + +相对于frp,nps等内网穿透工具有以下特点 + +1. 完美消除网络特征 +1. 防止服务端被主动探测 +1. 客户端和用户端内嵌配置,不需要命令行或额外配置文件 + +## 使用步骤 + +### 生成配置、客户端、用户端 + +`grss gen www.qq.com:443 127.0.0.1:443` + +1. `www.qq.com:443` 是被模拟的目标 +1. `127.0.0.1:443` 是服务器监听地址,这里要填写公网IP,端口最好和模拟目标一致 + +若SNIAddr或ServerAddr不指定,则尝试加载已有配置文件 + +```txt +Usage: + grss [OPTIONS] gen [gen-OPTIONS] [SNIAddr] [ServerAddr] + +generate server config and client + +Help Options: + -h, --help Show this help message + +[gen command options] + -d debug + -f=[chrome|firefox|safari|ios|android|edge|360|qq] client finger print (default: chrome) + -e= expire second (default: 30) + -o= server config output path (default: config.json) + --dir= client output directory (default: .) + +[gen command arguments] + SNIAddr: tls server address, e.g. example.com:443 + ServerAddr: server address, e.g. 8.8.8.8:443 +``` + +### 启动服务端 + +`grss serv` + +```txt +Usage: + grss [OPTIONS] serv [serv-OPTIONS] + +run server + +Help Options: + -h, --help Show this help message + +[serv command options] + -o= server config path (default: config.json) +``` + +### 启动客户端 + +`grsc` + +### 启动用户端 + +`grsu` + +```txt +Usage of grsu: + -l string + socks5 listen address (default "127.0.0.1:61080") +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..68d4773 --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +mkdir -p dist +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o ./cmd/grss/client/grsc_darwin ./cmd/grsc +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o ./cmd/grss/client/grsc_linux ./cmd/grsc +CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -trimpath -ldflags "-s -w" -o ./cmd/grss/client/grsc_windows.exe ./cmd/grsc + +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o ./cmd/grss/client/grsu_darwin ./cmd/grsu +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o ./cmd/grss/client/grsu_linux ./cmd/grsu +CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -trimpath -ldflags "-s -w" -o ./cmd/grss/client/grsu_windows.exe ./cmd/grsu + +go-bindata -nomemcopy -nometadata -prefix cmd/grss/client -o ./cmd/grss/files.go ./cmd/grss/client/ + +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -tags forceposix -trimpath -ldflags "-s -w" -o ./dist/grss_darwin ./cmd/grss +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags forceposix -trimpath -ldflags "-s -w" -o ./dist/grss_linux ./cmd/grss +CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -tags forceposix -trimpath -ldflags "-s -w" -o ./dist/grss_windows.exe ./cmd/grss + +cp README.md ./dist +cp README-REALITY.md ./dist \ No newline at end of file diff --git a/client.go b/client.go new file mode 100644 index 0000000..aff9f92 --- /dev/null +++ b/client.go @@ -0,0 +1,255 @@ +package reality + +import ( + "bytes" + "compress/zlib" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "io" + "net" + + utls "github.com/refraction-networking/utls" +) + +type ClientConfig struct { + ServerAddr string `json:"server_addr"` + SNI string `json:"sni_name"` + PublicKeyECDH string `json:"public_key_ecdh"` + PublicKeyVerify string `json:"public_key_verify"` + FingerPrint string `json:"finger_print,omitempty"` + ExpireSecond uint32 `json:"expire_second,omitempty"` + Debug bool `json:"debug,omitempty"` + + fingerPrint *utls.ClientHelloID // 客户端的TLS指纹 + publicKeyECDH *ecdh.PublicKey // 用于密钥协商 + publicKeyVerify ed25519.PublicKey // 用于验证服务器身份 +} + +var Fingerprints = map[string]*utls.ClientHelloID{ + "chrome": &utls.HelloChrome_Auto, + "firefox": &utls.HelloFirefox_Auto, + "safari": &utls.HelloSafari_Auto, + "ios": &utls.HelloIOS_Auto, + "android": &utls.HelloAndroid_11_OkHttp, + "edge": &utls.HelloEdge_Auto, + "360": &utls.Hello360_Auto, + "qq": &utls.HelloQQ_Auto, +} + +func (config *ClientConfig) Validate() error { + if config.ServerAddr == "" { + return errors.New("server ip is empty") + } + if config.SNI == "" { + return errors.New("server name is empty") + } + if config.PublicKeyECDH == "" { + return errors.New("public key ecdh is empty") + } + data, err := base64.StdEncoding.DecodeString(config.PublicKeyECDH) + if err != nil { + return err + } + config.publicKeyECDH, err = ecdh.X25519().NewPublicKey(data) + if err != nil { + return err + } + data, err = base64.StdEncoding.DecodeString(config.PublicKeyVerify) + if err != nil { + return err + } + config.publicKeyVerify = ed25519.PublicKey(data) + + if len(data) != ed25519.PublicKeySize { + return errors.New("public key verify length error") + } + if f, ok := Fingerprints[config.FingerPrint]; ok { + config.fingerPrint = f + } else { + config.fingerPrint = &utls.HelloChrome_Auto + } + if config.ExpireSecond == 0 { + config.ExpireSecond = DefaultExpireSecond + } + return nil +} + +func (config *ClientConfig) Marshal() ([]byte, error) { + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return nil, err + } + var buf bytes.Buffer + zipWrite := zlib.NewWriter(&buf) + if _, err = zipWrite.Write(data); err != nil { + return nil, err + } + if err = zipWrite.Close(); err != nil { + return nil, err + } + zipData := buf.Bytes() + if len(zipData) > 1022 { + return nil, errors.New("config data too large") + } + zipDataLen := uint16(len(zipData)) + configData := make([]byte, 1024) + configData[0] = byte(zipDataLen >> 8) + configData[1] = byte(zipDataLen & 0xff) + copy(configData[2:], zipData) + + return configData, nil +} + +func UnmarshalClientConfig(configData []byte) (*ClientConfig, error) { + zipDataLen := uint16(configData[0])<<8 | uint16(configData[1]) + if zipDataLen == 0 || zipDataLen > 1022 { + return nil, errors.New("invalid config length") + } + zipData := configData[2 : zipDataLen+2] + zipReader, err := zlib.NewReader(bytes.NewReader(zipData)) + if err != nil { + return nil, err + } + + zipData, err = io.ReadAll(zipReader) + if err != nil { + return nil, err + } + var config ClientConfig + err = json.Unmarshal(zipData, &config) + if err != nil { + return nil, err + } + if err := config.Validate(); err != nil { + return nil, err + } + return &config, nil +} + +func NewClient(ctx context.Context, config *ClientConfig, overlayData byte) (net.Conn, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + // 生成临时私钥 + priv, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + + // 与预共享公钥进行密钥协商,计算会话密钥 + sessionKey, err := priv.ECDH(config.publicKeyECDH) + if err != nil { + return nil, err + } + + // 根据会话密钥生成AEAD + block, err := aes.NewCipher(sessionKey) + if err != nil { + return nil, err + } + aead, err := cipher.NewGCMWithNonceSize(block, 8) + if err != nil { + return nil, err + } + nonce, err := generateNonce(aead.NonceSize(), sessionKey, config.ExpireSecond) + if err != nil { + return nil, err + } + // 加密数据 + plaintext := make([]byte, 16) + if _, err := rand.Read(plaintext); err != nil { + return nil, err + } + // 明文为16字节: REALITY + 随机数据 + copy(plaintext, Prefix) + // 密文为32字节 + ciphertext := aead.Seal(nil, nonce, plaintext, nil) + var dial net.Dialer + conn, err := dial.DialContext(ctx, "tcp", config.ServerAddr) + if err != nil { + return nil, err + } + logger := GetLogger(config.Debug) + uconn := utls.UClient( + conn, + &utls.Config{ + ServerName: config.SNI, + SessionTicketsDisabled: true, + MaxVersion: utls.VersionTLS12, + }, + *config.fingerPrint, + ) + // 构造Client Hello + if err := uconn.BuildHandshakeState(); err != nil { + conn.Close() + return nil, err + } + // 将临时公钥和加密数据发送给服务器,分别占用的Random和SessionId + hello := uconn.HandshakeState.Hello + hello.Random = priv.PublicKey().Bytes() + hello.SessionId = ciphertext + + // 已经做好私有握手准备,此时相关数据如下 + logger.Debugf("random(public for ecdh): %x", priv.PublicKey().Bytes()) + logger.Debugf("sessionId(ciphertext): %x", ciphertext) + logger.Debugf("sessionKey: %x", sessionKey) + logger.Debugf("nonce: %x", nonce) + logger.Debugf("plaintext: %x", plaintext) + + if err := uconn.HandshakeContext(ctx); err != nil { + uconn.Close() + return nil, err + } + state := uconn.ConnectionState() + logger.Debugf("version: %s,cipher: %s", utls.VersionName(state.Version), utls.CipherSuiteName(state.CipherSuite)) + is12 := state.Version == versionTLS12 + if is12 { + // 进行我们私有握手,客户端发送附加数据,服务端回复64字节签名数据 + logger.Debugf("overlayData: %x", overlayData) + // record数据前缀模仿seq + data := generateRandomData(seqNumerOne[:]) + data[len(data)-1] = overlayData + record := newTLSRecord(recordTypeApplicationData, versionTLS12, data) + if _, err := record.writeTo(uconn.GetUnderlyingConn()); err != nil { + uconn.Close() + return nil, err + } + record, err = readTlsRecord(uconn.GetUnderlyingConn()) + if err != nil { + return nil, err + } + if record.recordType != recordTypeApplicationData { + uconn.Close() + return nil, ErrVerifyFailed + } + if record.version != versionTLS12 { + uconn.Close() + return nil, ErrVerifyFailed + } + if len(record.recordData) < (64 + 8) { + uconn.Close() + return nil, ErrVerifyFailed + } + // 服务端回复64字节签名数据 + signature := record.recordData[8:(64 + 8)] + logger.Debugf("sign: %x", signature) + if !ed25519.Verify((ed25519.PublicKey)(config.publicKeyVerify), plaintext, signature) { + uconn.Close() + return nil, ErrVerifyFailed + } + // 服务端回复验证通过 + logger.Debugln("verify ok") + return newWarpConn(uconn.GetUnderlyingConn(), aead, overlayData, seqNumerOne), nil + } + uconn.Close() + return nil, ErrVerifyFailed + +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..19c346f --- /dev/null +++ b/client_test.go @@ -0,0 +1,66 @@ +package reality_test + +import ( + "context" + "crypto/ecdh" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "testing" + + "github.com/howmp/reality" +) + +func TestClient(t *testing.T) { + + privEcdh, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + pubVerify, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + config := &reality.ClientConfig{ + SNI: "www.qq.com", + ServerAddr: "www.qq.com:443", + PublicKeyECDH: base64.StdEncoding.EncodeToString(privEcdh.Bytes()), + PublicKeyVerify: base64.StdEncoding.EncodeToString(pubVerify), + Debug: true, + } + d, err := json.Marshal(config) + if err != nil { + t.Fatal(err) + } + t.Log(string(d)) + + _, err = reality.NewClient(context.Background(), config, 0) + if err == nil { + t.Fatal("should error") + } + +} + +func TestClientConfig(t *testing.T) { + configServer, err := reality.NewServerConfig("example.com:443", "127.0.0.1:443") + if err != nil { + t.Fatal(err) + } + config := configServer.ToClientConfig() + configData, err := config.Marshal() + if err != nil { + t.Fatal(err) + } + + newConfig, err := reality.UnmarshalClientConfig(configData) + if err != nil { + t.Fatal(err) + } + + if err := newConfig.Validate(); err != nil { + t.Fatal(err) + } + +} diff --git a/cmd/common.go b/cmd/common.go new file mode 100644 index 0000000..d56bda6 --- /dev/null +++ b/cmd/common.go @@ -0,0 +1,8 @@ +package cmd + +const ( + OverlayGRSC = byte(0x95) + OverlayGRSU = byte(0x27) +) + +var ConfigDataPlaceholder = []byte{0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} diff --git a/cmd/grsc/main.go b/cmd/grsc/main.go new file mode 100644 index 0000000..350e3a2 --- /dev/null +++ b/cmd/grsc/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "net" + "time" + + "github.com/armon/go-socks5" + "github.com/hashicorp/yamux" + "github.com/howmp/reality" + "github.com/howmp/reality/cmd" + "github.com/sirupsen/logrus" +) + +func main() { + config, err := reality.UnmarshalClientConfig(cmd.ConfigDataPlaceholder) + if err != nil { + println(err.Error()) + return + } + logger := reality.GetLogger(config.Debug) + logger.Infof("server addr: %s, sni: %s", config.ServerAddr, config.SNI) + + socksServer, err := socks5.New(&socks5.Config{}) + if err != nil { + logger.Fatalln(err) + } + c := client{logger: logger, config: config, socksServer: socksServer} + for { + err = c.serve() + if err != nil { + logger.Errorf("serve: %v", err) + } + logger.Infoln("sleep 5s") + time.Sleep(time.Second * 5) + + } +} + +type client struct { + logger logrus.FieldLogger + config *reality.ClientConfig + socksServer *socks5.Server +} + +func (c *client) serve() error { + c.logger.Infoln("try connect to server") + client, err := reality.NewClient(context.Background(), c.config, cmd.OverlayGRSC) + if err != nil { + return err + } + c.logger.Infoln("server connected") + defer client.Close() + session, err := yamux.Client(client, nil) + if err != nil { + return err + } + defer session.Close() + for { + stream, err := session.Accept() + if err != nil { + return err + } + c.logger.Infof("new client %s", stream.RemoteAddr()) + go c.handleStream(stream) + } +} + +func (c *client) handleStream(conn net.Conn) { + defer conn.Close() + c.socksServer.ServeConn(conn) +} diff --git a/cmd/grss/gen.go b/cmd/grss/gen.go new file mode 100644 index 0000000..2553b09 --- /dev/null +++ b/cmd/grss/gen.go @@ -0,0 +1,159 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "os" + "path/filepath" + + utls "github.com/refraction-networking/utls" + + "github.com/howmp/reality" + "github.com/howmp/reality/cmd" + "github.com/sirupsen/logrus" +) + +type gen struct { + Debug bool `short:"d" description:"debug"` + FingerPrint string `short:"f" default:"chrome" description:"client finger print" choice:"chrome" choice:"firefox" choice:"safari" choice:"ios" choice:"android" choice:"edge" choice:"360" choice:"qq"` + ExpireSecond uint32 `short:"e" default:"30" description:"expire second"` + ConfigPath string `short:"o" default:"config.json" description:"server config output path"` + ClientOutputDir string `long:"dir" default:"." description:"client output directory"` + Positional struct { + SNIAddr string `description:"tls server address, e.g. example.com:443"` + ServerAddr string `description:"server address, e.g. 8.8.8.8:443"` + } `positional-args:"yes"` + + logger logrus.FieldLogger +} + +func (c *gen) Execute(args []string) error { + c.logger = reality.GetLogger(c.Debug) + var config *reality.ServerConfig + var err error + if c.Positional.SNIAddr == "" || c.Positional.ServerAddr == "" { + c.logger.Infof("try loading config, path %s", c.ConfigPath) + config, err = loadConfig(c.ConfigPath) + if err != nil { + c.logger.Errorf("config load failed: %v", err) + return err + } + c.logger.Infof("config loaded") + c.Positional.SNIAddr = config.SNIAddr + c.Positional.ServerAddr = config.ServerAddr + + } else { + config, err = c.genConfig() + if err != nil { + return err + } + } + if err := c.check(); err != nil { + return err + } + return c.genClient(config.ToClientConfig()) + +} + +var cipherSuites = map[uint16]bool{ + utls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305: true, + utls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: true, + utls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: true, + utls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: true, + utls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: true, + utls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: true, + utls.TLS_RSA_WITH_AES_128_GCM_SHA256: true, + utls.TLS_RSA_WITH_AES_256_GCM_SHA384: true, +} + +func (c *gen) check() error { + logger := c.logger + logger.Infoln("checking", c.Positional.SNIAddr) + conn, err := utls.Dial("tcp", c.Positional.SNIAddr, &utls.Config{}) + if err != nil { + return err + } + defer conn.Close() + logger.Infoln("connected") + state := conn.ConnectionState() + logger.Infof("version: %s, ciphersuite: %s", utls.VersionName(state.Version), utls.CipherSuiteName(state.CipherSuite)) + if state.Version != utls.VersionTLS12 { + return errors.New("server must use tls 1.2") + } + + useAead := cipherSuites[state.CipherSuite] + if !useAead { + logger.Warnln("not use aead cipher suite") + } + logger.Infoln("server satisfied") + return nil +} + +func (c *gen) genConfig() (*reality.ServerConfig, error) { + + c.logger.Infof("generating config, path %s", c.ConfigPath) + config, err := reality.NewServerConfig(c.Positional.SNIAddr, c.Positional.ServerAddr) + if err != nil { + return nil, err + } + + config.Debug = c.Debug + config.ClientFingerPrint = c.FingerPrint + config.ExpireSecond = c.ExpireSecond + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return nil, err + } + if err := os.WriteFile(c.ConfigPath, data, 0644); err != nil { + return nil, err + } + return config, nil +} +func (c *gen) genClient(clientConfig *reality.ClientConfig) error { + c.logger.Infof("generating client, path %s", c.ClientOutputDir) + configData, err := clientConfig.Marshal() + if err != nil { + return err + } + + for _, name := range AssetNames() { + path := filepath.Join(c.ClientOutputDir, name) + ClientBin, err := replaceClientTemplate(MustAsset(name), configData) + if err != nil { + return err + } + if err := os.WriteFile(path, ClientBin, 0644); err != nil { + return err + } + c.logger.Infof("generated %s", path) + } + return nil +} + +func loadConfig(path string) (*reality.ServerConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + config := &reality.ServerConfig{} + if err := json.Unmarshal(data, config); err != nil { + return nil, err + } + if err := config.Validate(); err != nil { + return nil, err + } + return config, nil +} + +func replaceClientTemplate(template []byte, configData []byte) ([]byte, error) { + pos := bytes.Index(template, cmd.ConfigDataPlaceholder) + if pos == -1 { + return nil, errors.New("config not found") + } + buf := bytes.NewBuffer(make([]byte, 0, len(template))) + buf.Write(template[:pos]) + buf.Write(configData) + buf.Write(template[pos+len(configData):]) + return buf.Bytes(), nil +} diff --git a/cmd/grss/main.go b/cmd/grss/main.go new file mode 100644 index 0000000..8c5494c --- /dev/null +++ b/cmd/grss/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "os" + + "github.com/howmp/reality" + "github.com/jessevdk/go-flags" +) + +func main() { + p := flags.NewParser(nil, flags.PassAfterNonOption|flags.HelpFlag) + p.Name = "grss" + logger := reality.GetLogger(true) + p.AddCommand("gen", "generate server config and client", "generate server config and client", &gen{}) + p.AddCommand("serv", "run server", "run server", &serv{}) + writer := os.Stderr + _, err := p.Parse() + if err != nil { + if e, ok := err.(*flags.Error); ok { + if e.Type != flags.ErrHelp { + p.WriteHelp(writer) + } + writer.WriteString(fmt.Sprintln(err)) + } else { + logger.Fatal(err) + } + } + +} diff --git a/cmd/grss/serv.go b/cmd/grss/serv.go new file mode 100644 index 0000000..ff2d84b --- /dev/null +++ b/cmd/grss/serv.go @@ -0,0 +1,163 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net" + "sync" + + "github.com/hashicorp/yamux" + "github.com/howmp/reality" + "github.com/howmp/reality/cmd" + "github.com/sirupsen/logrus" +) + +type serv struct { + ConfigPath string `short:"o" default:"config.json" description:"server config path"` +} + +func (s *serv) Execute(args []string) error { + config, err := loadConfig(s.ConfigPath) + if err != nil { + return err + } + server := NewServer(config) + server.Serve() + return nil +} + +// Server 反向socks5代理服务端 +type Server struct { + config *reality.ServerConfig + portClientAddr string + logger logrus.FieldLogger + session *yamux.Session + sessionLock *sync.Mutex +} + +func NewServer(config *reality.ServerConfig) *Server { + return &Server{ + config: config, + portClientAddr: fmt.Sprintf(":%s", config.SNIPort()), + logger: reality.GetLogger(config.Debug), + sessionLock: &sync.Mutex{}, + } +} + +// Serve 监听端口,同时接收Reality客户端和用户连接 +func (s *Server) Serve() { + l, err := reality.Listen(s.portClientAddr, s.config) + if err != nil { + s.logger.Fatalf("reality listen: %v", err) + } + s.logger.Infof("reality listen %s", s.portClientAddr) + for { + conn, err := l.Accept() + if err != nil { + s.logger.Errorf("reality accept: %v", err) + continue + } + + if o, ok := conn.(reality.OverlayData); ok { + overlayData := o.OverlayData() + + if overlayData == cmd.OverlayGRSC { + s.logger.Infof("accept client %s", conn.RemoteAddr()) + go s.handleClient(conn) + continue + } else if overlayData == cmd.OverlayGRSU { + s.logger.Infof("accept user %s", conn.RemoteAddr()) + go s.handleUser(conn) + continue + } + } + s.logger.Warnf("accept %s, but overlay wrong", conn.RemoteAddr()) + conn.Close() + } +} + +func (s *Server) handleClient(conn net.Conn) { + if s.isSessionOpen() { + s.logger.Errorf("client session already open, close %s", conn.RemoteAddr()) + conn.Close() + return + } + s.sessionLock.Lock() + defer s.sessionLock.Unlock() + session, err := yamux.Server(conn, nil) + if err != nil { + s.logger.Error(err) + conn.Close() + } + go s.checkSession(session) + s.session = session + s.logger.Infof("session opened %s", conn.RemoteAddr()) +} + +func (s *Server) handleUser(conn net.Conn) { + defer conn.Close() + + session, err := yamux.Client(conn, nil) + if err != nil { + s.logger.Errorf("yamux: %v", err) + return + } + defer session.Close() + for { + stream, err := session.Accept() + if err != nil { + s.logger.Errorf("user session accept: %v", err) + return + } + s.logger.Infof("user stream accept %s", stream.RemoteAddr()) + go s.handleUserStream(stream) + + } +} +func (s *Server) handleUserStream(stream net.Conn) { + defer stream.Close() + conn, err := s.openClientSessionStream() + if err != nil { + s.logger.Errorf("open client session stream: %v", err) + return + } + defer conn.Close() + go io.Copy(conn, stream) + io.Copy(stream, conn) + +} + +func (s *Server) isSessionOpen() bool { + s.sessionLock.Lock() + defer s.sessionLock.Unlock() + if s.session != nil { + return !s.session.IsClosed() + } + return false +} + +func (s *Server) openClientSessionStream() (*yamux.Stream, error) { + s.sessionLock.Lock() + defer s.sessionLock.Unlock() + if s.session != nil { + stream, err := s.session.OpenStream() + if err != nil { + s.session.Close() + s.session = nil + return nil, err + } + return stream, nil + } + return nil, errors.New("client session not open") +} + +func (s *Server) checkSession(session *yamux.Session) { + <-session.CloseChan() + s.logger.Infof("client session closed %s", session.RemoteAddr()) + s.sessionLock.Lock() + defer s.sessionLock.Unlock() + if s.session == session { + s.session = nil + } +} diff --git a/cmd/grsu/main.go b/cmd/grsu/main.go new file mode 100644 index 0000000..a11245e --- /dev/null +++ b/cmd/grsu/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "errors" + "flag" + "io" + "net" + "time" + + "github.com/hashicorp/yamux" + "github.com/howmp/reality" + "github.com/howmp/reality/cmd" + "github.com/sirupsen/logrus" +) + +type serverSession struct { + config *reality.ClientConfig + session *yamux.Session + logger logrus.FieldLogger +} + +func newServerSession(config *reality.ClientConfig, logger logrus.FieldLogger) *serverSession { + return &serverSession{ + config: config, + logger: logger, + } +} + +func (s *serverSession) connectForever() { + + for { + s.connect() + s.logger.Infoln("sleep 5s") + time.Sleep(time.Second * 5) + } + +} +func (s *serverSession) connect() { + logger := s.logger + client, err := reality.NewClient(context.Background(), s.config, cmd.OverlayGRSU) + if err != nil { + logger.Errorf("connect server: %v", err) + return + } + defer client.Close() + session, err := yamux.Server(client, nil) + if err != nil { + logger.Errorf("yamux: %v", err) + return + } + defer session.Close() + s.session = session + logger.Infof("session opened %s", client.RemoteAddr()) + <-session.CloseChan() + logger.Infof("session closed %s", client.RemoteAddr()) +} + +func (s *serverSession) openSessionStream() (*yamux.Stream, error) { + + if s.session != nil { + stream, err := s.session.OpenStream() + if err != nil { + s.session.Close() + s.session = nil + return nil, err + } + return stream, nil + } + return nil, errors.New("session not open") +} + +func main() { + config, err := reality.UnmarshalClientConfig(cmd.ConfigDataPlaceholder) + if err != nil { + println(err.Error()) + return + } + + addr := flag.String("l", "127.0.0.1:61080", "socks5 listen address") + flag.Parse() + logger := reality.GetLogger(config.Debug) + l, err := net.Listen("tcp", *addr) + if err != nil { + logger.Panic(err) + } + logger.Infof("listen %s", *addr) + s := newServerSession(config, logger) + go s.connectForever() + for { + conn, err := l.Accept() + if err != nil { + logger.Errorf("accept: %v", err) + continue + } + go handleUser(conn, s, logger) + } +} + +func handleUser(conn net.Conn, s *serverSession, logger logrus.FieldLogger) { + defer conn.Close() + + stream, err := s.openSessionStream() + if err != nil { + logger.Errorf("open session stream: %v", err) + return + } + defer stream.Close() + + go io.Copy(stream, conn) + io.Copy(conn, stream) +} diff --git a/example/testcipher/main.go b/example/testcipher/main.go new file mode 100644 index 0000000..0118409 --- /dev/null +++ b/example/testcipher/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "fmt" +) + +func handshake() { + conn, err := tls.Dial("tcp", "www.qq.com:443", &tls.Config{}) + if err != nil { + panic(err) + } + err = conn.Handshake() + if err != nil { + panic(err) + } + // conn.Close() +} + +func main() { + signTest() + signTest2() + authkeyTest() + plaintext := make([]byte, 32) + if _, err := rand.Read(plaintext); err != nil { + panic(err) + } + copy(plaintext, []byte("REALITY")) + fmt.Println("plaintext", len(plaintext), plaintext) +} + +func signTest() { + fmt.Println("signTest") + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + data, err := x509.MarshalECPrivateKey(privateKey) + fmt.Println("priv", base64.RawURLEncoding.EncodeToString(data)) + if err != nil { + panic(err) + } + data, err = x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + if err != nil { + panic(err) + } + fmt.Println("publ", base64.RawURLEncoding.EncodeToString(data)) + hash := []byte("hello") + sign, err := ecdsa.SignASN1(rand.Reader, privateKey, hash[:]) + if err != nil { + panic(err) + } + fmt.Println("sign", len(sign), base64.RawURLEncoding.EncodeToString(sign)) + ok := ecdsa.VerifyASN1(&privateKey.PublicKey, hash[:], sign) + fmt.Println("verify", ok) + +} +func signTest2() { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + panic(err) + } + fmt.Println("priv", base64.RawURLEncoding.EncodeToString(priv)) + fmt.Println("publ", base64.RawURLEncoding.EncodeToString(pub)) + + msg := []byte("1") + + sign := ed25519.Sign(priv, msg) + fmt.Println("sign", len(sign), base64.RawURLEncoding.EncodeToString(sign)) + ok := ed25519.Verify(pub, msg, sign) + fmt.Println("verify", ok) + +} + +func authkeyTest() { + fmt.Println("authkeyTest") + // 服务端持有私钥,客户端持有公钥 + privateKeyServer, publicKeyClient := genEcdhX25519() + fmt.Println("priv", len(privateKeyServer), base64.RawURLEncoding.EncodeToString(privateKeyServer)) + fmt.Println("publ", len(publicKeyClient), base64.RawURLEncoding.EncodeToString(publicKeyClient)) + // tmp是每次Client Hello生成,其中公钥放在SessionID,私钥在客户端内存中 + privateKeyTmp, publicKeyTmp := genEcdhX25519() + + // 服务端进行密钥协商,得到authkey + authkey1, err := ecdhAuthKey(privateKeyServer, publicKeyTmp) + if err != nil { + panic(err) + } + + // 客户端进行密钥协商,得到authkey + authkey2, err := ecdhAuthKey(privateKeyTmp, publicKeyClient) + if err != nil { + panic(err) + } + fmt.Println(base64.RawURLEncoding.EncodeToString(authkey1)) + fmt.Println(base64.RawURLEncoding.EncodeToString(authkey2)) + + block, err := aes.NewCipher(authkey2) + if err != nil { + panic(err) + } + aead, err := cipher.NewGCM(block) + if err != nil { + panic(err) + } + fmt.Println("overhead", aead.Overhead(), "noncesize", aead.NonceSize()) + ciphertext := aead.Seal(nil, make([]byte, aead.NonceSize()), []byte("1234567890123456"), nil) + fmt.Println(len(ciphertext), base64.RawURLEncoding.EncodeToString(ciphertext)) + +} + +func ecdhAuthKey(privateKey []byte, publicKey []byte) ([]byte, error) { + + priv, err := ecdh.X25519().NewPrivateKey(privateKey) + if err != nil { + return nil, err + } + + pub, err := ecdh.X25519().NewPublicKey(publicKey) + if err != nil { + return nil, err + } + return priv.ECDH(pub) +} + +func genEcdhX25519() ([]byte, []byte) { + priv, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + return priv.Bytes(), priv.PublicKey().Bytes() + +} diff --git a/example/testclient/main.go b/example/testclient/main.go new file mode 100644 index 0000000..7f79496 --- /dev/null +++ b/example/testclient/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/howmp/reality" +) + +func main() { + logger := reality.GetLogger(false) + var config reality.ClientConfig + jsonData, err := os.ReadFile("config.json") + if err != nil { + logger.Panic(err) + } + if err := json.Unmarshal(jsonData, &config); err != nil { + logger.Panic(err) + } + + if err := config.Validate(); err != nil { + logger.Panic(err) + } + + client, err := reality.NewClient(context.Background(), &config, 0) + if err != nil { + logger.Panic(err) + } + defer client.Close() + reader := bufio.NewReader(client) + req, err := http.NewRequest("GET", "https://www.qq.com", nil) + if err != nil { + logger.Panic(err) + } + for i := 0; i < 10; i++ { + logger.Infoln("req", i) + req.URL.Path = fmt.Sprintf("/%d", i) + err = req.Write(client) + if err != nil { + logger.Panic(err) + } + resp, err := http.ReadResponse(reader, req) + if err != nil { + logger.Panic(err) + } + data, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + logger.Panic(err) + } + logger.Infoln("resp", string(data)) + + } + +} diff --git a/example/testserver/main.go b/example/testserver/main.go new file mode 100644 index 0000000..fb9d1f5 --- /dev/null +++ b/example/testserver/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + "github.com/howmp/reality" +) + +func main() { + logger := reality.GetLogger(false) + + if len(os.Args) < 2 { + log.Panic("usage: ./server 127.0.0.1:443") + } + addr := os.Args[1] + config, err := reality.NewServerConfig("www.qq.com:443", addr) + if err != nil { + log.Panic(err) + } + config.Debug = true + jsonData, err := json.MarshalIndent(config.ToClientConfig(), "", " ") + if err != nil { + log.Panic(err) + } + os.WriteFile("config.json", jsonData, 0644) + l, err := reality.Listen(addr, config) + if err != nil { + log.Panic(err) + } + httpServer := http.Server{ + Addr: addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger.Infof("req %s", r.RequestURI) + fmt.Fprintf(w, "hello") + }), + } + logger.Infof("listen %s", addr) + httpServer.Serve(l) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f7dd2dc --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/howmp/reality + +go 1.20 + +require ( + github.com/jessevdk/go-flags v1.6.1 + github.com/mattn/go-colorable v0.1.13 + github.com/refraction-networking/utls v1.6.7 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/crypto v0.27.0 +) + +require golang.org/x/net v0.23.0 // indirect + +require ( + github.com/andybalholm/brotli v1.0.6 // indirect + github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 + github.com/cloudflare/circl v1.3.7 // indirect + github.com/hashicorp/yamux v0.1.2 + github.com/klauspost/compress v1.17.4 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f33cd06 --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +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/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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 v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= +github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= +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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server.go b/server.go new file mode 100644 index 0000000..4226311 --- /dev/null +++ b/server.go @@ -0,0 +1,405 @@ +package reality + +import ( + "bufio" + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "io" + "net" + + "github.com/sirupsen/logrus" + "golang.org/x/crypto/cryptobyte" +) + +type ServerConfig struct { + SNIAddr string `json:"sni_addr"` + ServerAddr string `json:"server_addr"` + PrivateKeyECDH string `json:"private_key_ecdh"` + PrivateKeySign string `json:"private_key_sign"` + ExpireSecond uint32 `json:"expire_second"` + Debug bool `json:"debug"` + ClientFingerPrint string `json:"finger_print,omitempty"` + + privateKeyECDH *ecdh.PrivateKey + privateKeySign ed25519.PrivateKey + sniHost string + sniPort string +} + +func NewServerConfig(sniAddr string, serverAddr string) (*ServerConfig, error) { + privateKeyECDH, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + _, privateKeySign, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + sniHost, sniPort, err := net.SplitHostPort(sniAddr) + if err != nil { + return nil, err + } + return &ServerConfig{ + SNIAddr: sniAddr, + ServerAddr: serverAddr, + PrivateKeyECDH: base64.StdEncoding.EncodeToString(privateKeyECDH.Bytes()), + PrivateKeySign: base64.StdEncoding.EncodeToString(privateKeySign), + ExpireSecond: DefaultExpireSecond, + privateKeyECDH: privateKeyECDH, + privateKeySign: privateKeySign, + sniHost: sniHost, + sniPort: sniPort, + }, nil + +} + +func (c *ServerConfig) Validate() error { + if c.SNIAddr == "" { + return errors.New("SNI is required") + } + var err error + c.sniHost, c.sniPort, err = net.SplitHostPort(c.SNIAddr) + if err != nil { + return err + } + if c.ServerAddr == "" { + return errors.New("server address is required") + } + data, err := base64.StdEncoding.DecodeString(c.PrivateKeyECDH) + if err != nil { + return err + } + c.privateKeyECDH, err = ecdh.X25519().NewPrivateKey(data) + if err != nil { + return err + } + data, err = base64.StdEncoding.DecodeString(c.PrivateKeySign) + if err != nil { + return err + } + if len(data) != ed25519.PrivateKeySize { + return errors.New("private key sign length error") + } + c.privateKeySign = ed25519.PrivateKey(data) + + if c.ExpireSecond == 0 { + c.ExpireSecond = DefaultExpireSecond + } + + if c.ClientFingerPrint == "" { + c.ClientFingerPrint = "chrome" + } + return nil +} +func (c *ServerConfig) SNIHost() string { + return c.sniHost +} + +func (c *ServerConfig) SNIPort() string { + return c.sniPort +} +func (s *ServerConfig) ToClientConfig() *ClientConfig { + + return &ClientConfig{ + SNI: s.sniHost, + ServerAddr: s.ServerAddr, + PublicKeyECDH: base64.StdEncoding.EncodeToString(s.privateKeyECDH.PublicKey().Bytes()), + PublicKeyVerify: base64.StdEncoding.EncodeToString(s.privateKeySign.Public().(ed25519.PublicKey)), + ExpireSecond: s.ExpireSecond, + Debug: s.Debug, + FingerPrint: s.ClientFingerPrint, + } +} + +type Listener struct { + net.Listener + config *ServerConfig + chanConn chan net.Conn + chanErr chan error + logger logrus.FieldLogger +} + +func Listen(laddr string, config *ServerConfig) (net.Listener, error) { + inner, err := net.Listen("tcp", laddr) + if err != nil { + return nil, err + } + l := &Listener{ + Listener: inner, + config: config, + chanConn: make(chan net.Conn), + chanErr: make(chan error), + logger: GetLogger(config.Debug), + } + + go func() { + for { + conn, err := l.Listener.Accept() + if err != nil { + l.chanErr <- err + close(l.chanConn) + return + } + go func() { + c, err := l.handshake(conn) + if err != nil { + if l.config.Debug { + l.logger.Warnln("handshake", conn.RemoteAddr(), err) + } + } else { + l.chanConn <- c + } + }() + + } + }() + return l, nil +} +func (l *Listener) Accept() (net.Conn, error) { + if c, ok := <-l.chanConn; ok { + return c, nil + } + return nil, <-l.chanErr +} + +// handshake 尝试处理私有握手,失败则进行客户端和代理目标转发,成功返回加密包装后的客户端连接 +func (l *Listener) handshake(clientConn net.Conn) (net.Conn, error) { + logger := l.logger + targetConn, err := net.Dial("tcp", l.config.SNIAddr) + if err != nil { + return nil, errors.Join(ErrProxyDie, err) + } + // bufio.Reader是为了在读数据时,不是一个一个record读取,而是模仿一次性读取尽可能多的record + // io.TeeReader是为了在读数据时,同时互相转发 + clientReader := bufio.NewReader(io.TeeReader(clientConn, targetConn)) + targetReader := bufio.NewReader(io.TeeReader(targetConn, clientConn)) + var aead cipher.AEAD + var plaintext []byte + readClientHello := func() error { + recordClientHello, err := readTlsRecord(clientReader) + if err != nil { + return err + } + var random, sessionId []byte + s := cryptobyte.String(recordClientHello.recordData) + if !s.Skip(6) || // skip type(1) length(3) version(2) + !s.ReadBytes(&random, 32) || + !s.ReadUint8LengthPrefixed((*cryptobyte.String)(&sessionId)) || + len(sessionId) != 32 { + return fmt.Errorf("invalid client hello: %x", hex.EncodeToString(recordClientHello.recordData)) + } + logger.Debugf("random(public for ecdh): %x", random) + logger.Debugf("sessionId(ciphertext): %x", sessionId) + pub, err := ecdh.X25519().NewPublicKey(random) + if err != nil { + return err + } + sessionKey, err := l.config.privateKeyECDH.ECDH(pub) + if err != nil { + return err + } + logger.Debugf("sessionKey: %x", sessionKey) + + block, err := aes.NewCipher(sessionKey) + if err != nil { + return err + } + aead, err = cipher.NewGCMWithNonceSize(block, 8) + if err != nil { + return err + } + nonce, err := generateNonce(aead.NonceSize(), sessionKey, l.config.ExpireSecond) + if err != nil { + return err + } + logger.Debugf("nonce: %x", nonce) + + plaintext, err = aead.Open(nil, nonce, sessionId, nil) + if err != nil { + return err + } + logger.Debugf("plaintext: %x", plaintext) + + if !bytes.HasPrefix(plaintext, Prefix) { + return fmt.Errorf("invalid prefix: %x", plaintext[:len(Prefix)]) + } + logger.Debug("handshake ok") + return nil + } + if err = readClientHello(); err != nil { + go dup(clientConn, targetConn) + return nil, errors.Join(ErrVerifyFailed, err) + } + + if _, err = serverOrder1.wait(targetReader, logger); err != nil { + go dup(clientConn, targetConn) + return nil, err + } + + if _, err = clientOrder.wait(clientReader, logger); err != nil { + go dup(clientConn, targetConn) + return nil, err + } + records, err := serverOrder2.wait(targetReader, logger) + if err != nil { + go dup(clientConn, targetConn) + return nil, err + } + // 客户端和代理目标的tls握手已经完成,可以关闭目标的连接 + targetConn.Close() + + // 获取模拟目标的seq,如果有的话 + seq := [8]byte{} + copy(seq[:], seqNumerOne[:]) + if len(records) > 0 { + record := records[len(records)-1] + recordData := record.recordData + if len(recordData) > len(seq) { + copy(seq[:], recordData[:len(seq)]) + } + } + logger.Debugf("seqNumer: %x", seq) + incSeq(seq[:]) + + // 读取客户端发送的附加内容 + record, err := readTlsRecord(clientConn) + if err != nil { + return nil, err + } + overlayData := record.recordData[len(record.recordData)-1] + logger.Debugf("overlayData: %x", overlayData) + + // 发送服务端签名 + sign := ed25519.Sign(ed25519.PrivateKey(l.config.privateKeySign), plaintext) + logger.Debugf("sign: %x", sign) + record = newTLSRecord( + recordTypeApplicationData, versionTLS12, + generateRandomData(append(seq[:], sign...)), // record数据前缀模仿seq + ) + if _, err = record.writeTo(clientConn); err != nil { + clientConn.Close() + return nil, err + } + return newWarpConn(clientConn, aead, overlayData, seq), nil +} + +// dup 转发两个连接 +func dup(clientConn net.Conn, proxyConn net.Conn) { + defer clientConn.Close() + defer proxyConn.Close() + go io.Copy(proxyConn, clientConn) + io.Copy(clientConn, proxyConn) +} + +type recordOrders []struct { + recordType byte + handshakeType byte + optional bool +} + +var serverOrder1 = recordOrders{ + { + recordType: recordTypeHandshake, + handshakeType: typeServerHello, + }, + { + recordType: recordTypeHandshake, + handshakeType: typeCertificate, + }, + { + recordType: recordTypeHandshake, + handshakeType: typeServerKeyExchange, + }, + { + recordType: recordTypeHandshake, + handshakeType: typeServerHelloDone, + }, +} + +var clientOrder = recordOrders{ + { + recordType: recordTypeHandshake, + handshakeType: typeCertificate, + optional: true, + }, + { + recordType: recordTypeHandshake, + handshakeType: typeClientKeyExchange, + }, + { + recordType: recordTypeHandshake, + handshakeType: typeCertificateVerify, + optional: true, + }, + { + recordType: recordTypeChangeCipherSpec, + }, + { + recordType: recordTypeHandshake, // Encrypted Handshake Message(Finished) + }, +} + +var serverOrder2 = recordOrders{ + { + recordType: recordTypeHandshake, + handshakeType: typeNewSessionTicket, + optional: true, + }, + { + recordType: recordTypeChangeCipherSpec, + }, + { + + recordType: recordTypeHandshake, // Encrypted Handshake Message(Finished) + }, +} + +func (orders recordOrders) wait(reader io.Reader, logger logrus.FieldLogger) ([]*tlsRecord, error) { + records := make([]*tlsRecord, 0, len(orders)) + orderPos := 0 + for { + record, err := readTlsRecord(reader) + if err != nil { + return nil, err + } + records = append(records, record) + for pos := orderPos; pos < len(orders); pos++ { + o := orders[pos] + if o.handshakeType != 0 { + // 需要判断握手类型 + if len(record.recordData) != 0 && + record.recordData[0] == o.handshakeType { + orderPos = pos + 1 + break + } + } else { + orderPos = pos + 1 + break + } + + if o.optional { + // 如果当前类型是可选的,继续向下查找 + logger.Debugf("try optional record: %+v", record) + orderPos = pos + 1 + continue + } else { + return nil, fmt.Errorf( + "invalid record, want %+v, got %d %x,", + o, record.recordType, record.recordData, + ) + } + } + if orderPos == len(orders) { + return records, nil + } + } + +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..dc3f853 --- /dev/null +++ b/utils.go @@ -0,0 +1,254 @@ +package reality + +import ( + "bytes" + "crypto/cipher" + "crypto/sha256" + "encoding/binary" + "errors" + "io" + "math/rand" + "net" + "sync" + "time" + + "github.com/mattn/go-colorable" + utls "github.com/refraction-networking/utls" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/hkdf" +) + +var ( + ErrVerifyFailed = errors.New("verify failed") + ErrDecryptFailed = errors.New("decrypt failed") + ErrProxyDie = errors.New("proxy die") +) + +var Prefix = []byte("REALITY") + +const DefaultExpireSecond = 30 + +var seqNumerOne = [8]byte{0, 0, 0, 0, 0, 0, 0, 1} + +// generateNonce 根据SessionKey和ExpireSecond生成Nonce +func generateNonce(NonceSize int, SessionKey []byte, ExpireSecond uint32) ([]byte, error) { + info := make([]byte, 8) + binary.BigEndian.PutUint64(info, uint64(time.Now().Unix()%int64(ExpireSecond))) + nonce := make([]byte, NonceSize) + _, err := hkdf.New(sha256.New, SessionKey[:], Prefix, info).Read(nonce[:]) + if err != nil { + return nil, err + } + return nonce, nil +} + +var versionTLS12 = uint16(utls.VersionTLS12) + +const recordHeaderLen = 5 +const ( + recordTypeChangeCipherSpec = 20 + recordTypeAlert = 21 + recordTypeHandshake = 22 + recordTypeApplicationData = 23 +) + +const ( + typeHelloRequest uint8 = 0 + typeClientHello uint8 = 1 + typeServerHello uint8 = 2 + typeNewSessionTicket uint8 = 4 + typeEndOfEarlyData uint8 = 5 + typeEncryptedExtensions uint8 = 8 + typeCertificate uint8 = 11 + typeServerKeyExchange uint8 = 12 + typeCertificateRequest uint8 = 13 + typeServerHelloDone uint8 = 14 + typeCertificateVerify uint8 = 15 + typeClientKeyExchange uint8 = 16 + typeFinished uint8 = 20 + typeCertificateStatus uint8 = 22 + typeKeyUpdate uint8 = 24 +) + +type tlsRecord struct { + recordType uint8 + version uint16 + recordData []byte +} + +func newTLSRecord(recordType uint8, version uint16, recordData []byte) *tlsRecord { + return &tlsRecord{ + recordType: recordType, + version: version, + recordData: recordData, + } +} + +func (r *tlsRecord) marshal() []byte { + data := make([]byte, recordHeaderLen+len(r.recordData)) + data[0] = r.recordType + data[1] = byte(r.version >> 8) + data[2] = byte(r.version) + data[3] = byte(len(r.recordData) >> 8) + data[4] = byte(len(r.recordData)) + copy(data[5:], r.recordData) + return data +} + +func (r *tlsRecord) writeTo(w io.Writer) (int, error) { + n, err := bytes.NewReader(r.marshal()).WriteTo(w) + return int(n), err +} + +func readTlsRecord(reader io.Reader) (*tlsRecord, error) { + hdr := make([]byte, recordHeaderLen) + if _, err := io.ReadFull(reader, hdr); err != nil { + return nil, err + } + recordType := hdr[0] + if recordType < recordTypeChangeCipherSpec || recordType > recordTypeApplicationData { + return nil, errors.New("tls: unknown record type") + } + version := uint16(hdr[1])<<8 | uint16(hdr[2]) + if version < utls.VersionTLS10 || version > utls.VersionTLS13 { + return nil, errors.New("tls: unknown version") + } + recordLen := int(hdr[3])<<8 | int(hdr[4]) + + recordData := make([]byte, recordLen) + if _, err := io.ReadFull(reader, recordData); err != nil { + return nil, err + } + return &tlsRecord{ + recordType: recordType, + version: version, + recordData: recordData, + }, nil +} + +const maxSize = 1400 +const minSize = 900 + +var r = rand.New(rand.NewSource(time.Now().UnixNano())) + +// generateRandomData 生成随机900-1400数据 +func generateRandomData(prefix []byte) []byte { + len := r.Intn(maxSize-minSize+1) + minSize + data := make([]byte, len) + r.Read(data) + copy(data, prefix) + return data +} + +type OverlayData interface { + OverlayData() byte +} + +var _ OverlayData = (*warpConn)(nil) + +type warpConn struct { + net.Conn + aead cipher.AEAD + overlayData byte + seq []byte + lockRead *sync.Mutex + lockWrite *sync.Mutex + rawInput *bytes.Buffer + maxPayload int +} + +func newWarpConn(conn net.Conn, aead cipher.AEAD, overlayData byte, seq [8]byte) *warpConn { + incSeq(seq[:]) + w := &warpConn{ + Conn: conn, + lockRead: &sync.Mutex{}, + lockWrite: &sync.Mutex{}, + rawInput: &bytes.Buffer{}, + maxPayload: 0xFFFF - aead.Overhead() - recordHeaderLen, + aead: aead, + overlayData: overlayData, + seq: seq[:], + } + return w +} + +func (w *warpConn) Write(b []byte) (int, error) { + w.lockWrite.Lock() + defer w.lockWrite.Unlock() + wrote := 0 + for len(b) > 0 { + m := len(b) + if m > w.maxPayload { + m = w.maxPayload + } + data := w.aead.Seal(nil, w.seq[:], b[:m], nil) + data = append(w.seq[:], data...) + record := newTLSRecord(recordTypeApplicationData, versionTLS12, data) + incSeq(w.seq) + _, err := record.writeTo(w.Conn) + if err != nil { + return 0, err + } + wrote += m + b = b[m:] + } + return wrote, nil +} + +func (w *warpConn) Read(b []byte) (int, error) { + w.lockRead.Lock() + defer w.lockRead.Unlock() + if w.rawInput.Len() != 0 { + // 缓存中有数据,从缓存返回 + return w.rawInput.Read(b) + } + + record, err := readTlsRecord(w.Conn) + if err != nil { + return 0, err + } + if record.recordType != recordTypeApplicationData { + return 0, ErrVerifyFailed + } + if record.version != versionTLS12 { + return 0, ErrVerifyFailed + } + data := record.recordData + plaintext, err := w.aead.Open(nil, data[:8], data[8:], nil) + if err != nil { + return 0, err + } + n := copy(b, plaintext) + if n < len(plaintext) { + w.rawInput.Write(plaintext[n:]) + } + return n, nil +} + +func (w *warpConn) OverlayData() byte { + return w.overlayData +} + +func incSeq(seq []byte) { + for i := 7; i >= 0; i-- { + seq[i]++ + if seq[i] != 0 { + return + } + } +} + +func GetLogger(debug bool) logrus.FieldLogger { + level := logrus.InfoLevel + if debug { + level = logrus.DebugLevel + } + logger := logrus.New() + logger.SetLevel(level) + logger.SetOutput(colorable.NewColorableStderr()) + logger.Formatter = &logrus.TextFormatter{ + ForceColors: true, + DisableTimestamp: true, + } + return logger +}