mirror of
https://github.com/howmp/reality
synced 2025-02-23 02:22:15 +08:00
init
This commit is contained in:
commit
b8a9c7ad1a
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -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/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
70
README-REALITY.md
Normal file
70
README-REALITY.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
## reality
|
||||||
|
|
||||||
|
<https://github.com/XTLS/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
|
||||||
|
|
||||||
|
<https://github.com/XTLS/Xray-core/issues/1697#issuecomment-1441215569>
|
||||||
|
|
||||||
|
### reality的特点和限制
|
||||||
|
|
||||||
|
特点:
|
||||||
|
|
||||||
|
1. 完美模拟了伪装服务器的TLS指纹
|
||||||
|
1. 特殊客户端巧妙的利用TLS1.3的key_share和Session ID字段进行私有握手
|
||||||
|
1. 这两字段原本都是随机的,即使替换也没有特征
|
||||||
|
1. 不需要域名,也不需要证书
|
||||||
|
|
||||||
|
限制:
|
||||||
|
|
||||||
|
只能使用TLS1.3,且必须使用x25519
|
||||||
|
|
||||||
|
1. key_share是TLS1.3新增内容<https://www.rfc-editor.org/rfc/rfc8446#section-4.2.8>
|
||||||
|
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库,用读写过滤的方式来判断是否已经握手完成
|
79
README.md
Normal file
79
README.md
Normal file
@ -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")
|
||||||
|
```
|
19
build.sh
Executable file
19
build.sh
Executable file
@ -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
|
255
client.go
Normal file
255
client.go
Normal file
@ -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
|
||||||
|
|
||||||
|
}
|
66
client_test.go
Normal file
66
client_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
cmd/common.go
Normal file
8
cmd/common.go
Normal file
@ -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}
|
72
cmd/grsc/main.go
Normal file
72
cmd/grsc/main.go
Normal file
@ -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)
|
||||||
|
}
|
159
cmd/grss/gen.go
Normal file
159
cmd/grss/gen.go
Normal file
@ -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
|
||||||
|
}
|
30
cmd/grss/main.go
Normal file
30
cmd/grss/main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
163
cmd/grss/serv.go
Normal file
163
cmd/grss/serv.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
112
cmd/grsu/main.go
Normal file
112
cmd/grsu/main.go
Normal file
@ -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)
|
||||||
|
}
|
142
example/testcipher/main.go
Normal file
142
example/testcipher/main.go
Normal file
@ -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()
|
||||||
|
|
||||||
|
}
|
60
example/testclient/main.go
Normal file
60
example/testclient/main.go
Normal file
@ -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))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
example/testserver/main.go
Normal file
43
example/testserver/main.go
Normal file
@ -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)
|
||||||
|
}
|
23
go.mod
Normal file
23
go.mod
Normal file
@ -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
|
||||||
|
)
|
39
go.sum
Normal file
39
go.sum
Normal file
@ -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=
|
405
server.go
Normal file
405
server.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
254
utils.go
Normal file
254
utils.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user