├── LICENSE
├── README.md
├── address
└── addr.go
├── auth
└── auth.go
├── fragment
└── fragment.go
├── go.mod
├── go.sum
├── options
├── authenticate_option.go
├── connect_option.go
├── dissociate_option.go
├── interface.go
└── packet_option.go
├── protocol
└── protocol.go
└── utils
├── byte_util.go
├── certs_util.go
├── tls_util.go
└── util_test.go
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 The github.com/redis/go-redis Authors.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above
11 | copyright notice, this list of conditions and the following disclaimer
12 | in the documentation and/or other materials provided with the
13 | distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## TUIC-PROTOCOL-GO
2 |
3 | 本协议是[TUIC Protocol](https://github.com/EAimTY/tuic/blob/tuic-5.0.0/SPEC.md)的Golang实现版本,是以学习为目的开发和开源,一切用于违法行为的操作与作者无关
4 | Golang客户端:[tuic-client](https://github.com/ZYKJShadow/tuic-client)
5 | Golang服务器:[tuic-server](https://github.com/ZYKJShadow/tuic-server)
6 |
7 | ## 协议
8 | TUIC 协议依赖于一个可以多路复用的 TLS 加密流。所有的中继任务都通过 Command 中的 Header 来协商。
9 | ### 协议版本
10 | `0x05`
11 | ### Command
12 | ```plain
13 | +-----+------+----------+
14 | | VER | TYPE | OPT |
15 | +-----+------+----------+
16 | | 1 | 1 | Variable |
17 | +-----+------+----------+
18 | ```
19 | - VER: 协议版本,固定为`0x05`
20 | - TYPE: Command类型
21 | - OPT: Command参数,参考[Command 参数](#Command-参数)
22 | ### Command 类型
23 |
24 | 目前有五种Command类型:
25 | - `0x00` Authenticate - 认证
26 | - `0x01` Connect - 建立TCP中继
27 | - `0x02` Packet - 传输UDP中继(分片)
28 | - `0x03` Dissociate - 终止UDP中继
29 | - `0x04` Heartbeat - 心跳
30 |
31 | 其中 Connect 和 Packet 携带有效载荷(流/数据包片段)
32 | ### Command 参数
33 |
34 | #### `Authenticate`
35 | ```plain
36 | +------+-------+
37 | | UUID | TOKEN |
38 | +------+-------+
39 | | 16 | 32 |
40 | +------+-------+
41 | ```
42 | - `UUID` 客户端UUID
43 | - `TOKEN` 客户端令牌,客户端的UUID作为`label`,客户端密码作为`context`,从TLS连接中生成的32位密钥
44 |
45 | #### `Connect`
46 |
47 | ```plain
48 | +----------+
49 | | ADDR |
50 | +----------+
51 | | Variable |
52 | +----------+
53 | ```
54 |
55 | - `ADDR`: 目标地址信息,参考[地址信息](#地址信息)
56 |
57 | #### `Packet`
58 |
59 | ```plain
60 | +----------+--------+------------+---------+------+----------+
61 | | ASSOC_ID | PKT_ID | FRAG_TOTAL | FRAG_ID | SIZE | ADDR |
62 | +----------+--------+------------+---------+------+----------+
63 | | 2 | 2 | 1 | 1 | 2 | Variable |
64 | +----------+--------+------------+---------+------+----------+
65 | ```
66 |
67 | - `ASSOC_ID` - UDP中继的关联ID,参考[UDP中继](#3udp中继)
68 | - `PKT_ID` - UDP包ID
69 | - `FRAG_TOTAL` - UDP包分段总数
70 | - `FRAG_ID` - UDP包分段ID
71 | - `SIZE` - UDP分段包大小
72 | - `ADDR` - 目标地址信息,参考[地址信息](#地址信息)
73 |
74 |
75 | #### `Dissociate`
76 |
77 | ```plain
78 | +----------+
79 | | ASSOC_ID |
80 | +----------+
81 | | 2 |
82 | +----------+
83 | ```
84 | - `ASSOC_ID` - UDP中继的关联ID,参考[UDP中继](#3udp中继)
85 |
86 | #### `Heartbeat`
87 | ```plain
88 | +-+
89 | | |
90 | +-+
91 | | |
92 | +-+
93 | ```
94 |
95 | ### 地址信息
96 | ### `ADDR`
97 | ```plain
98 | +------+-------------+----------+
99 | | TYPE | ADDRESS | PORT |
100 | +------+-------------+----------+
101 | | 1 | Variable | 2 |
102 | +------+-------------+----------+
103 | ```
104 |
105 | - `TYPE` - 地址类型
106 | - `ADDRESS` - 地址
107 | - `PORT` - 端口号
108 |
109 | 地址类型:
110 | - `0xff`:无
111 | - `0x00`:域名(类型为域名时,ADDRESS部分的第一个字节表示域名长度)
112 | - `0x01`:IPv4 地址
113 | - `0x02`:IPv6 地址
114 |
115 | ## 整体流程
116 |
117 | ### 1、认证
118 | - 客户端打开一个单向流(uni_stream),发送`Authenticate`命令
119 | - 服务器接收到`Authenticate`后,验证 Token 的有效性。如果 Token 有效,则连接认证成功,进行其他中继任务
120 | - 如果服务器在接收到`Authenticate`命令之前收到其他命令,则只接受命令头部分并等待认证,认证成功后通知正在等待的协程继续进行任务
121 |
122 | ### 2、TCP中继
123 | - 客户端打开一个双向流(bi_stream),发送`Connect`命令
124 | - 客户端在`Connect`命令发送成功之后,立刻对双向流和本地连接建立双向传输
125 | - 服务器接收到`Connect`命令后,打开一个到目标地址的TCP流,建立成功后,服务器立即在TCP流和双向流之间传输数据
126 |
127 | ### 3、UDP中继
128 | - `ASSOC_ID`由客户端生成
129 | - 客户端可以通过QUIC 单向流(quic模式)或 QUIC datagram(native模式)发送`Packet`命令
130 | - 通过在客户端和服务器之间同步UDP`ASSOC_ID`来实现0-RTT
131 | - 客户端和服务器为每个 QUIC 连接创建一个 UDP 会话表,将每个`ASSOC_ID`映射到一个UDP socket
132 | - 服务器为每一个`ASSOC_ID`分配一个UDP socket。服务器使用这个UDP socket发送客户端请求的 UDP 数据包,同时接收来自任何目的地的UDP数据包,并添加`Packet`命令头发送回客户端
133 | - 客户端可以通过 QUIC 单向流发送`Dissociate`命令来解除关联
134 |
135 | ### 4、 心跳
136 | 客户端定期通过Datagram的方式发送`Heartbeat`命令
137 |
138 |
--------------------------------------------------------------------------------
/address/addr.go:
--------------------------------------------------------------------------------
1 | package address
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/binary"
7 | "errors"
8 | "fmt"
9 | "github.com/ZYKJShadow/tuic-protocol-go/utils"
10 | "io"
11 | "net"
12 | )
13 |
14 | const (
15 | AddrTypeNone = 0xff
16 | AddrTypeDomain = 0x00
17 | AddrTypeIPv4 = 0x01
18 | AddrTypeIPv6 = 0x02
19 | )
20 |
21 | type Address interface {
22 | TypeCode() uint8
23 | Len() int
24 | String() string
25 | Marshal() ([]byte, error)
26 | ResolveDNS() ([]net.TCPAddr, error)
27 | }
28 |
29 | var _ Address = (*NoneAddress)(nil)
30 |
31 | type NoneAddress struct{}
32 |
33 | func (a *NoneAddress) Marshal() ([]byte, error) {
34 | return nil, nil
35 | }
36 | func (a *NoneAddress) TypeCode() uint8 { return 0xff }
37 | func (a *NoneAddress) Len() int { return 1 }
38 | func (a *NoneAddress) String() string { return "none" }
39 | func (a *NoneAddress) ResolveDNS() ([]net.TCPAddr, error) {
40 | return nil, errors.New("none address")
41 | }
42 |
43 | var _ Address = (*DomainAddress)(nil)
44 |
45 | type DomainAddress struct {
46 | Domain string
47 | Port uint16
48 | }
49 |
50 | func (a *DomainAddress) Marshal() ([]byte, error) {
51 | var buf bytes.Buffer
52 | buf.WriteByte(a.TypeCode())
53 | buf.WriteByte(byte(len(a.Domain)))
54 | buf.WriteString(a.Domain)
55 | err := binary.Write(&buf, binary.BigEndian, a.Port)
56 | if err != nil {
57 | return nil, err
58 | }
59 |
60 | return buf.Bytes(), nil
61 | }
62 |
63 | func (a *DomainAddress) TypeCode() uint8 { return 0x00 }
64 | func (a *DomainAddress) Len() int { return 1 + 1 + len(a.Domain) + 2 }
65 | func (a *DomainAddress) String() string { return fmt.Sprintf("%s:%d", a.Domain, a.Port) }
66 | func (a *DomainAddress) ResolveDNS() ([]net.TCPAddr, error) {
67 | ips, err := net.DefaultResolver.LookupIPAddr(context.Background(), a.Domain)
68 | if err != nil {
69 | return nil, err
70 | }
71 | var result []net.TCPAddr
72 | for _, ip := range ips {
73 | result = append(result, net.TCPAddr{
74 | IP: ip.IP,
75 | Port: int(a.Port),
76 | })
77 | }
78 | return result, nil
79 | }
80 |
81 | var _ Address = (*SocketAddress)(nil)
82 |
83 | type SocketAddress struct {
84 | Addr net.TCPAddr
85 | }
86 |
87 | func (a *SocketAddress) Marshal() ([]byte, error) {
88 | var buf bytes.Buffer
89 | buf.WriteByte(a.TypeCode())
90 |
91 | if a.Addr.IP.To4() != nil {
92 | buf.Write(a.Addr.IP.To4())
93 | } else {
94 | buf.Write(a.Addr.IP)
95 | }
96 |
97 | err := binary.Write(&buf, binary.BigEndian, uint16(a.Addr.Port))
98 | if err != nil {
99 | return nil, err
100 | }
101 |
102 | return buf.Bytes(), nil
103 | }
104 |
105 | func (a *SocketAddress) TypeCode() uint8 {
106 | if a.Addr.IP.To4() != nil {
107 | return 0x01
108 | }
109 | return 0x02
110 | }
111 |
112 | func (a *SocketAddress) Len() int {
113 | if a.Addr.IP.To4() != nil {
114 | return 1 + 4 + 2
115 | }
116 | return 1 + 16 + 2
117 | }
118 |
119 | func (a *SocketAddress) String() string { return a.Addr.String() }
120 |
121 | func (a *SocketAddress) ResolveDNS() ([]net.TCPAddr, error) {
122 | return []net.TCPAddr{a.Addr}, nil
123 | }
124 |
125 | func UnMarshalAddr(r io.Reader) (Address, error) {
126 | var buf [1]byte
127 | _, err := io.ReadFull(r, buf[:])
128 | if err != nil {
129 | return nil, err
130 | }
131 |
132 | typeCode := buf[0]
133 |
134 | switch typeCode {
135 | case AddrTypeNone:
136 | return &NoneAddress{}, nil
137 | case AddrTypeDomain:
138 | _, err = io.ReadFull(r, buf[:])
139 | if err != nil {
140 | return nil, err
141 | }
142 |
143 | lenBytes := int(buf[0])
144 |
145 | data := make([]byte, lenBytes+2)
146 | _, err = io.ReadFull(r, data)
147 | if err != nil {
148 | return nil, err
149 | }
150 |
151 | port := utils.ReadUint16(data[lenBytes:])
152 | domain := string(data[:lenBytes])
153 |
154 | return &DomainAddress{Domain: domain, Port: port}, nil
155 | case AddrTypeIPv4:
156 | var b [6]byte
157 | _, err = io.ReadFull(r, b[:])
158 | if err != nil {
159 | return nil, err
160 | }
161 | ip := net.IPv4(b[0], b[1], b[2], b[3])
162 | port := binary.BigEndian.Uint16(b[4:])
163 | return &SocketAddress{Addr: net.TCPAddr{IP: ip, Port: int(port)}}, nil
164 | case AddrTypeIPv6:
165 | var b [18]byte
166 | _, err := io.ReadFull(r, b[:])
167 | if err != nil {
168 | return nil, err
169 | }
170 | ip := make(net.IP, net.IPv6len)
171 | for i := 0; i < net.IPv6len; i += 2 {
172 | ip[i] = b[i]
173 | ip[i+1] = b[i+1]
174 | }
175 | port := binary.BigEndian.Uint16(b[16:])
176 | return &SocketAddress{Addr: net.TCPAddr{IP: ip, Port: int(port)}}, nil
177 | default:
178 | return nil, errors.New("unsupported address type")
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/auth/auth.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import "github.com/quic-go/quic-go"
4 |
5 | type Authenticated struct {
6 | store quic.TokenStore
7 | gets chan<- string
8 | puts chan<- string
9 | }
10 |
11 | var _ quic.TokenStore = (*Authenticated)(nil)
12 |
13 | func NewAuthenticated(store quic.TokenStore, gets, puts chan<- string) *Authenticated {
14 | return &Authenticated{
15 | store: store,
16 | gets: gets,
17 | puts: puts,
18 | }
19 | }
20 |
21 | func (a *Authenticated) Pop(key string) (token *quic.ClientToken) {
22 | a.gets <- key
23 | return a.store.Pop(key)
24 | }
25 |
26 | func (a *Authenticated) Put(key string, token *quic.ClientToken) {
27 | a.puts <- key
28 | a.store.Put(key, token)
29 | }
30 |
--------------------------------------------------------------------------------
/fragment/fragment.go:
--------------------------------------------------------------------------------
1 | package fragment
2 |
3 | import (
4 | "sort"
5 | "sync"
6 | )
7 |
8 | type FBuffer struct {
9 | fragments map[uint8][]byte
10 | total uint8
11 | size uint16
12 | }
13 |
14 | type FCache struct {
15 | cache map[uint16]*FBuffer
16 | sync.Mutex
17 | }
18 |
19 | func NewFCache() *FCache {
20 | return &FCache{
21 | cache: make(map[uint16]*FBuffer),
22 | }
23 | }
24 |
25 | func NewFragmentBuffer(total uint8, size uint16) *FBuffer {
26 | return &FBuffer{
27 | fragments: make(map[uint8][]byte),
28 | total: total,
29 | size: size,
30 | }
31 | }
32 |
33 | func (fc *FCache) DelFragment(assocID uint16) {
34 | fc.Lock()
35 | defer fc.Unlock()
36 |
37 | delete(fc.cache, assocID)
38 | }
39 |
40 | func (fc *FCache) AddFragment(assocID uint16, fragID uint8, total uint8, size uint16, data []byte) []byte {
41 | fc.Lock()
42 | defer fc.Unlock()
43 |
44 | fb, ok := fc.cache[assocID]
45 | if !ok {
46 | fb = NewFragmentBuffer(total, size)
47 | fc.cache[assocID] = fb
48 | } else {
49 | fb.size += size
50 | }
51 |
52 | if fb.SetFragData(fragID, data).IsComplete() {
53 | assembled := fb.Assemble()
54 | delete(fc.cache, assocID)
55 | return assembled
56 | }
57 |
58 | return nil
59 | }
60 |
61 | func (fb *FBuffer) SetFragData(fragID uint8, data []byte) *FBuffer {
62 | fb.fragments[fragID] = data
63 | return fb
64 | }
65 |
66 | func (fb *FBuffer) IsComplete() bool {
67 | return uint8(len(fb.fragments)) == fb.total
68 | }
69 |
70 | func (fb *FBuffer) Assemble() []byte {
71 | keys := make([]uint8, 0, len(fb.fragments))
72 | for k := range fb.fragments {
73 | keys = append(keys, k)
74 | }
75 |
76 | sort.Slice(keys, func(i, j int) bool {
77 | return keys[i] < keys[j]
78 | })
79 |
80 | assembled := make([]byte, fb.size)
81 | pos := 0
82 | for _, k := range keys {
83 | copy(assembled[pos:], fb.fragments[k])
84 | pos += len(fb.fragments[k])
85 | }
86 |
87 | return assembled
88 | }
89 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ZYKJShadow/tuic-protocol-go
2 |
3 | go 1.21.7
4 |
5 | require (
6 | github.com/quic-go/quic-go v0.44.0
7 | github.com/sirupsen/logrus v1.9.3
8 | )
9 |
10 | require (
11 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
12 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
13 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect
14 | go.uber.org/mock v0.4.0 // indirect
15 | golang.org/x/crypto v0.23.0 // indirect
16 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
17 | golang.org/x/mod v0.17.0 // indirect
18 | golang.org/x/net v0.25.0 // indirect
19 | golang.org/x/sys v0.20.0 // indirect
20 | golang.org/x/tools v0.21.0 // indirect
21 | )
22 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
8 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
9 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
10 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
15 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
16 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
17 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
18 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
19 | github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
20 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
21 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24 | github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
25 | github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
26 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
27 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
29 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
30 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
31 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
32 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
33 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
34 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
35 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
36 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
37 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
38 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
39 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
40 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
41 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
42 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
43 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
44 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
46 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
47 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
48 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
49 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
50 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
51 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
52 | golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
53 | golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
54 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
55 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
57 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
58 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
59 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
60 |
--------------------------------------------------------------------------------
/options/authenticate_option.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | type AuthenticateOptions struct {
4 | UUID []byte
5 | Token []byte
6 | }
7 |
8 | var _ IOption = (*AuthenticateOptions)(nil)
9 |
10 | func (a *AuthenticateOptions) Marshal() ([]byte, error) {
11 | b := make([]byte, 48)
12 | copy(b[:16], a.UUID[:])
13 | copy(b[16:], a.Token[:])
14 | return b, nil
15 | }
16 |
17 | func (a *AuthenticateOptions) Unmarshal(b []byte) error {
18 | a.UUID = make([]byte, 16)
19 | a.Token = make([]byte, 32)
20 |
21 | copy(a.UUID[:], b[:16])
22 | copy(a.Token[:], b[16:])
23 | return nil
24 | }
25 |
26 | func (a *AuthenticateOptions) Len() uint32 {
27 | return 16 + 32
28 | }
29 |
--------------------------------------------------------------------------------
/options/connect_option.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import (
4 | "bytes"
5 | "github.com/ZYKJShadow/tuic-protocol-go/address"
6 | )
7 |
8 | type ConnectOptions struct {
9 | Addr address.Address
10 | }
11 |
12 | var _ IOption = (*ConnectOptions)(nil)
13 |
14 | func (c *ConnectOptions) Marshal() ([]byte, error) {
15 | return c.Addr.Marshal()
16 | }
17 |
18 | func (c *ConnectOptions) Unmarshal(b []byte) error {
19 | addr, err := address.UnMarshalAddr(bytes.NewReader(b))
20 | if err != nil {
21 | return err
22 | }
23 |
24 | c.Addr = addr
25 | return nil
26 | }
27 |
28 | func (c *ConnectOptions) Len() uint32 {
29 | return uint32(c.Addr.Len())
30 | }
31 |
--------------------------------------------------------------------------------
/options/dissociate_option.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import (
4 | "github.com/ZYKJShadow/tuic-protocol-go/utils"
5 | )
6 |
7 | type DissociateOptions struct {
8 | AssocID uint16
9 | }
10 |
11 | var _ IOption = (*DissociateOptions)(nil)
12 |
13 | func (d *DissociateOptions) Marshal() ([]byte, error) {
14 | b := make([]byte, 2)
15 | utils.WriteUint16(b, d.AssocID)
16 | return b, nil
17 | }
18 |
19 | func (d *DissociateOptions) Unmarshal(b []byte) error {
20 | d.AssocID = utils.ReadUint16(b)
21 | return nil
22 | }
23 |
24 | func (d *DissociateOptions) Len() uint32 {
25 | return 2
26 | }
27 |
--------------------------------------------------------------------------------
/options/interface.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | type IOption interface {
4 | Marshal() ([]byte, error)
5 | Unmarshal(b []byte) error
6 | Len() uint32
7 | }
8 |
--------------------------------------------------------------------------------
/options/packet_option.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import (
4 | "errors"
5 | "github.com/ZYKJShadow/tuic-protocol-go/address"
6 | "github.com/ZYKJShadow/tuic-protocol-go/utils"
7 | )
8 |
9 | type PacketOptions struct {
10 | AssocID uint16
11 | PacketID uint16
12 | FragTotal uint8
13 | FragID uint8
14 | Size uint16
15 | Addr address.Address
16 | }
17 |
18 | var _ IOption = (*PacketOptions)(nil)
19 |
20 | func (p *PacketOptions) Marshal() ([]byte, error) {
21 | b := make([]byte, 8+p.Addr.Len())
22 | utils.WriteUint16(b[0:2], p.AssocID)
23 | utils.WriteUint16(b[2:4], p.PacketID)
24 | utils.WriteUint8(b[4:5], p.FragTotal)
25 | utils.WriteUint8(b[5:6], p.FragID)
26 | utils.WriteUint16(b[6:8], p.Size)
27 |
28 | addr, err := p.Addr.Marshal()
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | copy(b[8:], addr)
34 |
35 | return b, nil
36 | }
37 |
38 | func (p *PacketOptions) Unmarshal(b []byte) error {
39 | if len(b) < 8 {
40 | return errors.New("invalid packet options length")
41 | }
42 |
43 | p.AssocID = utils.ReadUint16(b[0:2])
44 | p.PacketID = utils.ReadUint16(b[2:4])
45 | p.FragTotal = utils.ReadUint8(b[4:5])
46 | p.FragID = utils.ReadUint8(b[5:6])
47 | p.Size = utils.ReadUint16(b[6:8])
48 |
49 | return nil
50 | }
51 |
52 | func (p *PacketOptions) Len() uint32 {
53 | return 2 + 2 + 1 + 1 + 2 + uint32(p.Addr.Len())
54 | }
55 |
56 | func (p *PacketOptions) CalFragTotal(payload []byte, maxPktSize uint32) {
57 | dataLen := uint32(len(payload))
58 | if dataLen < maxPktSize {
59 | p.FragTotal = 1
60 | } else {
61 | p.FragTotal = uint8(dataLen / maxPktSize)
62 | if dataLen%maxPktSize != 0 {
63 | p.FragTotal++
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/protocol/protocol.go:
--------------------------------------------------------------------------------
1 | package protocol
2 |
3 | import (
4 | "errors"
5 | "github.com/ZYKJShadow/tuic-protocol-go/address"
6 | "github.com/ZYKJShadow/tuic-protocol-go/options"
7 | "github.com/quic-go/quic-go"
8 | "github.com/sirupsen/logrus"
9 | "io"
10 | )
11 |
12 | const VersionMajor = 0x05
13 |
14 | const (
15 | CmdAuthenticate = 0x00
16 | CmdConnect = 0x01
17 | CmdPacket = 0x02
18 | CmdDissociate = 0x03
19 | CmdHeartbeat = 0x04
20 | )
21 |
22 | const (
23 | HeaderLen = 2
24 | PacketLen = 8
25 | AuthenticateLen = 48
26 | DissociateLen = 2
27 | )
28 |
29 | const (
30 | NetworkTcp = "tcp"
31 | NetworkUdp = "udp"
32 | )
33 |
34 | //goland:noinspection ALL
35 | const (
36 | UdpRelayModeQuic = "quic"
37 | UdpRelayModeNative = "native"
38 | )
39 |
40 | const (
41 | NormalClosed = quic.StreamErrorCode(0)
42 | ClientCanceled = quic.StreamErrorCode(1)
43 | ServerCanceled = quic.StreamErrorCode(2)
44 | )
45 |
46 | const DefaultConcurrentStreams int64 = 32
47 |
48 | type Command struct {
49 | Version uint8
50 | Type uint8
51 | Options options.IOption
52 | }
53 |
54 | type PacketResponse struct {
55 | PacketID uint32
56 | Data []byte
57 | }
58 |
59 | func (cmd *Command) Marshal() ([]byte, error) {
60 | // 创建一个字节数组,长度为2(版本号和类型各占1字节)+2(Options长度占2字节)+Options的长度
61 | totalLen := HeaderLen
62 | if cmd.Options != nil {
63 | totalLen += int(cmd.Options.Len())
64 | }
65 |
66 | cmdBytes := make([]byte, totalLen)
67 |
68 | // 将版本号写入第1个字节
69 | cmdBytes[0] = cmd.Version
70 |
71 | // 将类型写入第2个字节
72 | cmdBytes[1] = cmd.Type
73 |
74 | if cmd.Options != nil {
75 | // 将Options写入剩余的字节
76 | b, err := cmd.Options.Marshal()
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | copy(cmdBytes[2:], b)
82 | }
83 |
84 | return cmdBytes, nil
85 | }
86 |
87 | func (cmd *Command) Unmarshal(stream io.Reader) error {
88 | header := make([]byte, HeaderLen)
89 | _, err := io.ReadFull(stream, header)
90 | if err != nil {
91 | logrus.Errorf("header io.ReadFull err:%v", err)
92 | return err
93 | }
94 |
95 | cmd.Version = header[0]
96 | cmd.Type = header[1]
97 |
98 | switch cmd.Type {
99 | case CmdAuthenticate:
100 | authBytes := make([]byte, AuthenticateLen)
101 | _, err = io.ReadFull(stream, authBytes)
102 | if err != nil {
103 | logrus.Errorf("authBytes io.ReadFull err:%v", err)
104 | return err
105 | }
106 |
107 | var opt options.AuthenticateOptions
108 | err = opt.Unmarshal(authBytes)
109 | if err != nil {
110 | logrus.Errorf("opt.Unmarshal err:%v", err)
111 | return err
112 | }
113 |
114 | cmd.Options = &opt
115 | case CmdConnect:
116 | protocolAddr, err := address.UnMarshalAddr(stream)
117 | if err != nil {
118 | logrus.Errorf("address.UnMarshalAddr err:%v", err)
119 | return err
120 | }
121 |
122 | var opt options.ConnectOptions
123 | opt.Addr = protocolAddr
124 | cmd.Options = &opt
125 | case CmdPacket:
126 | packetBytes := make([]byte, PacketLen)
127 | _, err = io.ReadFull(stream, packetBytes)
128 | if err != nil {
129 | logrus.Errorf("packetBytes io.ReadFull err:%v", err)
130 | return err
131 | }
132 |
133 | var opt options.PacketOptions
134 | err = opt.Unmarshal(packetBytes)
135 | if err != nil {
136 | logrus.Errorf("opt.Unmarshal err:%v", err)
137 | return err
138 | }
139 |
140 | protocolAddr, err := address.UnMarshalAddr(stream)
141 | if err != nil {
142 | logrus.Errorf("address.UnMarshalAddr err:%v", err)
143 | return err
144 | }
145 |
146 | opt.Addr = protocolAddr
147 |
148 | cmd.Options = &opt
149 |
150 | case CmdDissociate:
151 | dissociateBytes := make([]byte, DissociateLen)
152 | _, err = io.ReadFull(stream, dissociateBytes)
153 | if err != nil {
154 | logrus.Errorf("dissociateBytes io.ReadFull err:%v", err)
155 | return err
156 | }
157 |
158 | var opt options.DissociateOptions
159 | err = opt.Unmarshal(dissociateBytes)
160 | if err != nil {
161 | logrus.Errorf("opt.Unmarshal err:%v", err)
162 | return err
163 | }
164 |
165 | cmd.Options = &opt
166 |
167 | case CmdHeartbeat:
168 |
169 | default:
170 | return errors.New("unknown command type")
171 | }
172 |
173 | return nil
174 | }
175 |
--------------------------------------------------------------------------------
/utils/byte_util.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "encoding/binary"
4 |
5 | func ReadUint16(b []byte) uint16 {
6 | return binary.BigEndian.Uint16(b)
7 | }
8 |
9 | func WriteUint16(b []byte, v uint16) {
10 | binary.BigEndian.PutUint16(b, v)
11 | }
12 |
13 | func ReadUint8(b []byte) uint8 {
14 | return b[0]
15 | }
16 |
17 | func WriteUint8(b []byte, v uint8) {
18 | b[0] = v
19 | }
20 |
--------------------------------------------------------------------------------
/utils/certs_util.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "crypto/x509/pkix"
8 | "encoding/pem"
9 | "fmt"
10 | "math/big"
11 | "net"
12 | "os"
13 | "time"
14 | )
15 |
16 | func GenerateCert(host string, ip string) error {
17 | // 生成 RSA 私钥
18 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
19 | if err != nil {
20 | return err
21 | }
22 |
23 | // 创建证书模板
24 | template := x509.Certificate{
25 | SerialNumber: big.NewInt(1),
26 | Subject: pkix.Name{
27 | CommonName: host,
28 | },
29 | NotBefore: time.Now(),
30 | NotAfter: time.Now().Add(365 * 24 * time.Hour),
31 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
32 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
33 | IPAddresses: []net.IP{net.ParseIP(ip)},
34 | }
35 |
36 | // 创建自签名证书
37 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
38 | if err != nil {
39 | return err
40 | }
41 |
42 | // 将证书写入文件
43 | certOut, err := os.Create("cert.pem")
44 | if err != nil {
45 | return err
46 | }
47 |
48 | err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
49 | if err != nil {
50 | return err
51 | }
52 |
53 | err = certOut.Close()
54 | if err != nil {
55 | return err
56 | }
57 |
58 | // 将私钥写入文件
59 | keyOut, err := os.Create("key.pem")
60 | if err != nil {
61 | return err
62 | }
63 |
64 | err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
65 | if err != nil {
66 | return err
67 | }
68 |
69 | err = keyOut.Close()
70 | if err != nil {
71 | return err
72 | }
73 |
74 | return nil
75 | }
76 |
77 | func AddSelfSignedCertToClientPool(certFile string) (*x509.CertPool, error) {
78 | if certFile == "" {
79 | return nil, nil
80 | }
81 |
82 | caCert, err := os.ReadFile(certFile)
83 | if err != nil {
84 | return nil, err
85 | }
86 |
87 | certPool := x509.NewCertPool()
88 | if ok := certPool.AppendCertsFromPEM(caCert); !ok {
89 | return nil, fmt.Errorf("failed to append self-signed certificate to pool")
90 | }
91 |
92 | return certPool, nil
93 | }
94 |
--------------------------------------------------------------------------------
/utils/tls_util.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/x509"
5 | "encoding/pem"
6 | "errors"
7 | "os"
8 | )
9 |
10 | func LoadCerts(certFile string) ([][]byte, error) {
11 | if certFile == "" {
12 | return nil, nil
13 | }
14 |
15 | certPEM, err := os.ReadFile(certFile)
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | certs, err := parseCertsPEM(certPEM)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | return certs, nil
26 | }
27 |
28 | func LoadPrivateKey(keyFile string) (any, error) {
29 | if keyFile == "" {
30 | return nil, nil
31 | }
32 |
33 | keyPEM, err := os.ReadFile(keyFile)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | key, err := parsePrivateKeyPEM(keyPEM)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | return key, nil
44 | }
45 |
46 | func parseCertsPEM(certsPEM []byte) ([][]byte, error) {
47 | var certs [][]byte
48 | var cert []byte
49 | for len(certsPEM) > 0 {
50 | var block *pem.Block
51 | block, certsPEM = pem.Decode(certsPEM)
52 | if block == nil {
53 | break
54 | }
55 | cert = append(cert, block.Bytes...)
56 | if len(certsPEM) == 0 {
57 | certs = append(certs, cert)
58 | }
59 | }
60 | return certs, nil
61 | }
62 |
63 | func parsePrivateKeyPEM(keyPEM []byte) (any, error) {
64 | block, _ := pem.Decode(keyPEM)
65 | if block == nil {
66 | return nil, errors.New("failed to decode PEM block")
67 | }
68 |
69 | switch block.Type {
70 | case "RSA PRIVATE KEY":
71 | return x509.ParsePKCS1PrivateKey(block.Bytes)
72 | case "EC PRIVATE KEY":
73 | return x509.ParseECPrivateKey(block.Bytes)
74 | default:
75 | return nil, errors.New("unsupported private key type")
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/utils/util_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "testing"
4 |
5 | func TestGenerateCert(t *testing.T) {
6 | err := GenerateCert("localhost", "127.0.0.1")
7 | if err != nil {
8 | t.Errorf("GenerateCert() error = %v", err)
9 | return
10 | }
11 | }
12 |
--------------------------------------------------------------------------------