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