This commit is contained in:
howmp 2024-10-10 11:08:23 +08:00
commit b8a9c7ad1a
20 changed files with 2050 additions and 0 deletions

30
.gitignore vendored Normal file
View 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
View 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
View 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
View 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)
相对于frpnps等内网穿透工具有以下特点
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}

View 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))
}
}

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