mirror of
https://github.com/apernet/OpenGFW.git
synced 2024-11-14 22:39:26 +08:00
feat: quic analyzer (client side only)
This commit is contained in:
parent
7a52228ec6
commit
c1e90960dd
205
analyzer/internal/tls.go
Normal file
205
analyzer/internal/tls.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/apernet/OpenGFW/analyzer"
|
||||||
|
"github.com/apernet/OpenGFW/analyzer/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseTLSClientHello(chBuf *utils.ByteBuffer) analyzer.PropMap {
|
||||||
|
var ok bool
|
||||||
|
m := make(analyzer.PropMap)
|
||||||
|
// Version, random & session ID length combined are within 35 bytes,
|
||||||
|
// so no need for bounds checking
|
||||||
|
m["version"], _ = chBuf.GetUint16(false, true)
|
||||||
|
m["random"], _ = chBuf.Get(32, true)
|
||||||
|
sessionIDLen, _ := chBuf.GetByte(true)
|
||||||
|
m["session"], ok = chBuf.Get(int(sessionIDLen), true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for session ID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cipherSuitesLen, ok := chBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for cipher suites length
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cipherSuitesLen%2 != 0 {
|
||||||
|
// Cipher suites are 2 bytes each, so must be even
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ciphers := make([]uint16, cipherSuitesLen/2)
|
||||||
|
for i := range ciphers {
|
||||||
|
ciphers[i], ok = chBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m["ciphers"] = ciphers
|
||||||
|
compressionMethodsLen, ok := chBuf.GetByte(true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for compression methods length
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Compression methods are 1 byte each, we just put a byte slice here
|
||||||
|
m["compression"], ok = chBuf.Get(int(compressionMethodsLen), true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for compression methods
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
extsLen, ok := chBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// No extensions, I guess it's possible?
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
extBuf, ok := chBuf.GetSubBuffer(int(extsLen), true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for extensions
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for extBuf.Len() > 0 {
|
||||||
|
extType, ok := extBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for extension type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
extLen, ok := extBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for extension length
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
||||||
|
if !ok || !parseTLSExtensions(extType, extDataBuf, m) {
|
||||||
|
// Not enough data for extension data, or invalid extension
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTLSServerHello(shBuf *utils.ByteBuffer) analyzer.PropMap {
|
||||||
|
var ok bool
|
||||||
|
m := make(analyzer.PropMap)
|
||||||
|
// Version, random & session ID length combined are within 35 bytes,
|
||||||
|
// so no need for bounds checking
|
||||||
|
m["version"], _ = shBuf.GetUint16(false, true)
|
||||||
|
m["random"], _ = shBuf.Get(32, true)
|
||||||
|
sessionIDLen, _ := shBuf.GetByte(true)
|
||||||
|
m["session"], ok = shBuf.Get(int(sessionIDLen), true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for session ID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cipherSuite, ok := shBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for cipher suite
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m["cipher"] = cipherSuite
|
||||||
|
compressionMethod, ok := shBuf.GetByte(true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for compression method
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m["compression"] = compressionMethod
|
||||||
|
extsLen, ok := shBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// No extensions, I guess it's possible?
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
extBuf, ok := shBuf.GetSubBuffer(int(extsLen), true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for extensions
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for extBuf.Len() > 0 {
|
||||||
|
extType, ok := extBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for extension type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
extLen, ok := extBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for extension length
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
||||||
|
if !ok || !parseTLSExtensions(extType, extDataBuf, m) {
|
||||||
|
// Not enough data for extension data, or invalid extension
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTLSExtensions(extType uint16, extDataBuf *utils.ByteBuffer, m analyzer.PropMap) bool {
|
||||||
|
switch extType {
|
||||||
|
case 0x0000: // SNI
|
||||||
|
ok := extDataBuf.Skip(2) // Ignore list length, we only care about the first entry for now
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for list length
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sniType, ok := extDataBuf.GetByte(true)
|
||||||
|
if !ok || sniType != 0 {
|
||||||
|
// Not enough data for SNI type, or not hostname
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sniLen, ok := extDataBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for SNI length
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m["sni"], ok = extDataBuf.GetString(int(sniLen), true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for SNI
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case 0x0010: // ALPN
|
||||||
|
ok := extDataBuf.Skip(2) // Ignore list length, as we read until the end
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for list length
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var alpnList []string
|
||||||
|
for extDataBuf.Len() > 0 {
|
||||||
|
alpnLen, ok := extDataBuf.GetByte(true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for ALPN length
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
alpn, ok := extDataBuf.GetString(int(alpnLen), true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for ALPN
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
alpnList = append(alpnList, alpn)
|
||||||
|
}
|
||||||
|
m["alpn"] = alpnList
|
||||||
|
case 0x002b: // Supported Versions
|
||||||
|
if extDataBuf.Len() == 2 {
|
||||||
|
// Server only selects one version
|
||||||
|
m["supported_versions"], _ = extDataBuf.GetUint16(false, true)
|
||||||
|
} else {
|
||||||
|
// Client sends a list of versions
|
||||||
|
ok := extDataBuf.Skip(1) // Ignore list length, as we read until the end
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for list length
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var versions []uint16
|
||||||
|
for extDataBuf.Len() > 0 {
|
||||||
|
ver, ok := extDataBuf.GetUint16(false, true)
|
||||||
|
if !ok {
|
||||||
|
// Not enough data for version
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
versions = append(versions, ver)
|
||||||
|
}
|
||||||
|
m["supported_versions"] = versions
|
||||||
|
}
|
||||||
|
case 0xfe0d: // ECH
|
||||||
|
// We can't parse ECH for now, just set a flag
|
||||||
|
m["ech"] = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
@ -2,6 +2,7 @@ package tcp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
"github.com/apernet/OpenGFW/analyzer"
|
||||||
|
"github.com/apernet/OpenGFW/analyzer/internal"
|
||||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
"github.com/apernet/OpenGFW/analyzer/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -142,74 +143,14 @@ func (s *tlsStream) parseClientHello() utils.LSMAction {
|
|||||||
// Not a full client hello yet
|
// Not a full client hello yet
|
||||||
return utils.LSMActionPause
|
return utils.LSMActionPause
|
||||||
}
|
}
|
||||||
s.reqUpdated = true
|
m := internal.ParseTLSClientHello(chBuf)
|
||||||
s.reqMap = make(analyzer.PropMap)
|
if m == nil {
|
||||||
// Version, random & session ID length combined are within 35 bytes,
|
|
||||||
// so no need for bounds checking
|
|
||||||
s.reqMap["version"], _ = chBuf.GetUint16(false, true)
|
|
||||||
s.reqMap["random"], _ = chBuf.Get(32, true)
|
|
||||||
sessionIDLen, _ := chBuf.GetByte(true)
|
|
||||||
s.reqMap["session"], ok = chBuf.Get(int(sessionIDLen), true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for session ID
|
|
||||||
return utils.LSMActionCancel
|
return utils.LSMActionCancel
|
||||||
}
|
} else {
|
||||||
cipherSuitesLen, ok := chBuf.GetUint16(false, true)
|
s.reqUpdated = true
|
||||||
if !ok {
|
s.reqMap = m
|
||||||
// Not enough data for cipher suites length
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
if cipherSuitesLen%2 != 0 {
|
|
||||||
// Cipher suites are 2 bytes each, so must be even
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
ciphers := make([]uint16, cipherSuitesLen/2)
|
|
||||||
for i := range ciphers {
|
|
||||||
ciphers[i], ok = chBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.reqMap["ciphers"] = ciphers
|
|
||||||
compressionMethodsLen, ok := chBuf.GetByte(true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for compression methods length
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
// Compression methods are 1 byte each, we just put a byte slice here
|
|
||||||
s.reqMap["compression"], ok = chBuf.Get(int(compressionMethodsLen), true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for compression methods
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
extsLen, ok := chBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// No extensions, I guess it's possible?
|
|
||||||
return utils.LSMActionNext
|
return utils.LSMActionNext
|
||||||
}
|
}
|
||||||
extBuf, ok := chBuf.GetSubBuffer(int(extsLen), true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for extensions
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
for extBuf.Len() > 0 {
|
|
||||||
extType, ok := extBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for extension type
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
extLen, ok := extBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for extension length
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
|
||||||
if !ok || !s.handleExtensions(extType, extDataBuf, s.reqMap) {
|
|
||||||
// Not enough data for extension data, or invalid extension
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *tlsStream) parseServerHello() utils.LSMAction {
|
func (s *tlsStream) parseServerHello() utils.LSMAction {
|
||||||
@ -218,131 +159,14 @@ func (s *tlsStream) parseServerHello() utils.LSMAction {
|
|||||||
// Not a full server hello yet
|
// Not a full server hello yet
|
||||||
return utils.LSMActionPause
|
return utils.LSMActionPause
|
||||||
}
|
}
|
||||||
s.respUpdated = true
|
m := internal.ParseTLSServerHello(shBuf)
|
||||||
s.respMap = make(analyzer.PropMap)
|
if m == nil {
|
||||||
// Version, random & session ID length combined are within 35 bytes,
|
|
||||||
// so no need for bounds checking
|
|
||||||
s.respMap["version"], _ = shBuf.GetUint16(false, true)
|
|
||||||
s.respMap["random"], _ = shBuf.Get(32, true)
|
|
||||||
sessionIDLen, _ := shBuf.GetByte(true)
|
|
||||||
s.respMap["session"], ok = shBuf.Get(int(sessionIDLen), true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for session ID
|
|
||||||
return utils.LSMActionCancel
|
return utils.LSMActionCancel
|
||||||
}
|
} else {
|
||||||
cipherSuite, ok := shBuf.GetUint16(false, true)
|
s.respUpdated = true
|
||||||
if !ok {
|
s.respMap = m
|
||||||
// Not enough data for cipher suite
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.respMap["cipher"] = cipherSuite
|
|
||||||
compressionMethod, ok := shBuf.GetByte(true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for compression method
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.respMap["compression"] = compressionMethod
|
|
||||||
extsLen, ok := shBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// No extensions, I guess it's possible?
|
|
||||||
return utils.LSMActionNext
|
return utils.LSMActionNext
|
||||||
}
|
}
|
||||||
extBuf, ok := shBuf.GetSubBuffer(int(extsLen), true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for extensions
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
for extBuf.Len() > 0 {
|
|
||||||
extType, ok := extBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for extension type
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
extLen, ok := extBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for extension length
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
|
||||||
if !ok || !s.handleExtensions(extType, extDataBuf, s.respMap) {
|
|
||||||
// Not enough data for extension data, or invalid extension
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tlsStream) handleExtensions(extType uint16, extDataBuf *utils.ByteBuffer, m analyzer.PropMap) bool {
|
|
||||||
switch extType {
|
|
||||||
case 0x0000: // SNI
|
|
||||||
ok := extDataBuf.Skip(2) // Ignore list length, we only care about the first entry for now
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for list length
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
sniType, ok := extDataBuf.GetByte(true)
|
|
||||||
if !ok || sniType != 0 {
|
|
||||||
// Not enough data for SNI type, or not hostname
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
sniLen, ok := extDataBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for SNI length
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
m["sni"], ok = extDataBuf.GetString(int(sniLen), true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for SNI
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case 0x0010: // ALPN
|
|
||||||
ok := extDataBuf.Skip(2) // Ignore list length, as we read until the end
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for list length
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var alpnList []string
|
|
||||||
for extDataBuf.Len() > 0 {
|
|
||||||
alpnLen, ok := extDataBuf.GetByte(true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for ALPN length
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
alpn, ok := extDataBuf.GetString(int(alpnLen), true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for ALPN
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
alpnList = append(alpnList, alpn)
|
|
||||||
}
|
|
||||||
m["alpn"] = alpnList
|
|
||||||
case 0x002b: // Supported Versions
|
|
||||||
if extDataBuf.Len() == 2 {
|
|
||||||
// Server only selects one version
|
|
||||||
m["supported_versions"], _ = extDataBuf.GetUint16(false, true)
|
|
||||||
} else {
|
|
||||||
// Client sends a list of versions
|
|
||||||
ok := extDataBuf.Skip(1) // Ignore list length, as we read until the end
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for list length
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var versions []uint16
|
|
||||||
for extDataBuf.Len() > 0 {
|
|
||||||
ver, ok := extDataBuf.GetUint16(false, true)
|
|
||||||
if !ok {
|
|
||||||
// Not enough data for version
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
versions = append(versions, ver)
|
|
||||||
}
|
|
||||||
m["supported_versions"] = versions
|
|
||||||
}
|
|
||||||
case 0xfe0d: // ECH
|
|
||||||
// We can't parse ECH for now, just set a flag
|
|
||||||
m["ech"] = true
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *tlsStream) Close(limited bool) *analyzer.PropUpdate {
|
func (s *tlsStream) Close(limited bool) *analyzer.PropUpdate {
|
||||||
|
31
analyzer/udp/internal/quic/LICENSE
Normal file
31
analyzer/udp/internal/quic/LICENSE
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
Author:: Cuong Manh Le <cuong.manhle.vn@gmail.com>
|
||||||
|
Copyright:: Copyright (c) 2023, Cuong Manh Le
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the @organization@ nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG
|
||||||
|
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||||
|
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||||
|
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||||
|
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
1
analyzer/udp/internal/quic/README.md
Normal file
1
analyzer/udp/internal/quic/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
The code here is from https://github.com/cuonglm/quicsni with various modifications.
|
105
analyzer/udp/internal/quic/header.go
Normal file
105
analyzer/udp/internal/quic/header.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package quic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/quic-go/quic-go/quicvarint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Header represents a QUIC header.
|
||||||
|
type Header struct {
|
||||||
|
Type uint8
|
||||||
|
Version uint32
|
||||||
|
SrcConnectionID []byte
|
||||||
|
DestConnectionID []byte
|
||||||
|
Length int64
|
||||||
|
Token []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseInitialHeader parses the initial packet of a QUIC connection,
|
||||||
|
// return the initial header and number of bytes read so far.
|
||||||
|
func ParseInitialHeader(data []byte) (*Header, int64, error) {
|
||||||
|
br := bytes.NewReader(data)
|
||||||
|
hdr, err := parseLongHeader(br)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
n := int64(len(data) - br.Len())
|
||||||
|
return hdr, n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLongHeader(b *bytes.Reader) (*Header, error) {
|
||||||
|
typeByte, err := b.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h := &Header{}
|
||||||
|
ver, err := beUint32(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.Version = ver
|
||||||
|
if h.Version != 0 && typeByte&0x40 == 0 {
|
||||||
|
return nil, errors.New("not a QUIC packet")
|
||||||
|
}
|
||||||
|
destConnIDLen, err := b.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.DestConnectionID = make([]byte, int(destConnIDLen))
|
||||||
|
if err := readConnectionID(b, h.DestConnectionID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
srcConnIDLen, err := b.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.SrcConnectionID = make([]byte, int(srcConnIDLen))
|
||||||
|
if err := readConnectionID(b, h.SrcConnectionID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
initialPacketType := byte(0b00)
|
||||||
|
if h.Version == V2 {
|
||||||
|
initialPacketType = 0b01
|
||||||
|
}
|
||||||
|
if (typeByte >> 4 & 0b11) == initialPacketType {
|
||||||
|
tokenLen, err := quicvarint.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tokenLen > uint64(b.Len()) {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
h.Token = make([]byte, tokenLen)
|
||||||
|
if _, err := io.ReadFull(b, h.Token); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pl, err := quicvarint.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.Length = int64(pl)
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConnectionID(r io.Reader, cid []byte) error {
|
||||||
|
_, err := io.ReadFull(r, cid)
|
||||||
|
if err == io.ErrUnexpectedEOF {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func beUint32(r io.Reader) (uint32, error) {
|
||||||
|
b := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(r, b); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(b), nil
|
||||||
|
}
|
193
analyzer/udp/internal/quic/packet_protector.go
Normal file
193
analyzer/udp/internal/quic/packet_protector.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package quic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20"
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewProtectionKey creates a new ProtectionKey.
|
||||||
|
func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
|
||||||
|
return newProtectionKey(suite, secret, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key
|
||||||
|
// is used for encrypt/decrypt Initial Packet only.
|
||||||
|
//
|
||||||
|
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets
|
||||||
|
func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) {
|
||||||
|
return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPacketProtector creates a new PacketProtector.
|
||||||
|
func NewPacketProtector(key *ProtectionKey) *PacketProtector {
|
||||||
|
return &PacketProtector{key: key}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacketProtector is used for protecting a QUIC packet.
|
||||||
|
//
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection
|
||||||
|
type PacketProtector struct {
|
||||||
|
key *ProtectionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnProtect decrypts a QUIC packet.
|
||||||
|
func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) {
|
||||||
|
if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 {
|
||||||
|
return nil, errors.New("packet with long header is too small")
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample
|
||||||
|
sampleOffset := pnOffset + 4
|
||||||
|
sample := packet[sampleOffset : sampleOffset+16]
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati
|
||||||
|
mask := pp.key.headerProtection(sample)
|
||||||
|
if isLongHeader(packet[0]) {
|
||||||
|
// Long header: 4 bits masked
|
||||||
|
packet[0] ^= mask[0] & 0x0f
|
||||||
|
} else {
|
||||||
|
// Short header: 5 bits masked
|
||||||
|
packet[0] ^= mask[0] & 0x1f
|
||||||
|
}
|
||||||
|
|
||||||
|
pnLen := packet[0]&0x3 + 1
|
||||||
|
pn := int64(0)
|
||||||
|
for i := uint8(0); i < pnLen; i++ {
|
||||||
|
packet[pnOffset:][i] ^= mask[1+i]
|
||||||
|
pn = (pn << 8) | int64(packet[pnOffset:][i])
|
||||||
|
}
|
||||||
|
pn = decodePacketNumber(pnMax, pn, pnLen)
|
||||||
|
hdr := packet[:pnOffset+int64(pnLen)]
|
||||||
|
payload := packet[pnOffset:][pnLen:]
|
||||||
|
dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decryption failed: %w", err)
|
||||||
|
}
|
||||||
|
return dec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtectionKey is the key used to protect a QUIC packet.
|
||||||
|
type ProtectionKey struct {
|
||||||
|
aead cipher.AEAD
|
||||||
|
headerProtection func(sample []byte) (mask []byte)
|
||||||
|
iv []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage
|
||||||
|
//
|
||||||
|
// "The 62 bits of the reconstructed QUIC packet number in network byte order are
|
||||||
|
// left-padded with zeros to the size of the IV. The exclusive OR of the padded
|
||||||
|
// packet number and the IV forms the AEAD nonce."
|
||||||
|
func (pk *ProtectionKey) nonce(pn int64) []byte {
|
||||||
|
nonce := make([]byte, len(pk.iv))
|
||||||
|
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn))
|
||||||
|
for i := range pk.iv {
|
||||||
|
nonce[i] ^= pk.iv[i]
|
||||||
|
}
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
|
||||||
|
switch suite {
|
||||||
|
case tls.TLS_AES_128_GCM_SHA256:
|
||||||
|
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16)
|
||||||
|
c, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
aead, err := cipher.NewGCM(c)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
|
||||||
|
hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16)
|
||||||
|
hp, err := aes.NewCipher(hpKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
k := &ProtectionKey{}
|
||||||
|
k.aead = aead
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection
|
||||||
|
k.headerProtection = func(sample []byte) []byte {
|
||||||
|
mask := make([]byte, hp.BlockSize())
|
||||||
|
hp.Encrypt(mask, sample)
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
k.iv = iv
|
||||||
|
return k, nil
|
||||||
|
case tls.TLS_CHACHA20_POLY1305_SHA256:
|
||||||
|
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize)
|
||||||
|
aead, err := chacha20poly1305.New(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
|
||||||
|
hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize)
|
||||||
|
k := &ProtectionKey{}
|
||||||
|
k.aead = aead
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote
|
||||||
|
k.headerProtection = func(sample []byte) []byte {
|
||||||
|
nonce := sample[4:16]
|
||||||
|
c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
c.SetCounter(binary.LittleEndian.Uint32(sample[:4]))
|
||||||
|
mask := make([]byte, 5)
|
||||||
|
c.XORKeyStream(mask, mask)
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
k.iv = iv
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("not supported cipher suite")
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodePacketNumber decode the packet number after header protection removed.
|
||||||
|
//
|
||||||
|
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a
|
||||||
|
func decodePacketNumber(largest, truncated int64, nbits uint8) int64 {
|
||||||
|
expected := largest + 1
|
||||||
|
win := int64(1 << (nbits * 8))
|
||||||
|
hwin := win / 2
|
||||||
|
mask := win - 1
|
||||||
|
candidate := (expected &^ mask) | truncated
|
||||||
|
switch {
|
||||||
|
case candidate <= expected-hwin && candidate < (1<<62)-win:
|
||||||
|
return candidate + win
|
||||||
|
case candidate > expected+hwin && candidate >= win:
|
||||||
|
return candidate - win
|
||||||
|
}
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from crypto/tls/key_schedule.go.
|
||||||
|
func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte {
|
||||||
|
var hkdfLabel cryptobyte.Builder
|
||||||
|
hkdfLabel.AddUint16(uint16(length))
|
||||||
|
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes([]byte("tls13 "))
|
||||||
|
b.AddBytes([]byte(label))
|
||||||
|
})
|
||||||
|
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes(context)
|
||||||
|
})
|
||||||
|
out := make([]byte, length)
|
||||||
|
n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out)
|
||||||
|
if err != nil || n != length {
|
||||||
|
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
94
analyzer/udp/internal/quic/packet_protector_test.go
Normal file
94
analyzer/udp/internal/quic/packet_protector_test.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package quic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitialPacketProtector_UnProtect(t *testing.T) {
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial
|
||||||
|
protect := mustHexDecodeString(`
|
||||||
|
c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7
|
||||||
|
6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498
|
||||||
|
44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5
|
||||||
|
be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e
|
||||||
|
fd8142fafc0f76
|
||||||
|
`)
|
||||||
|
unProtect := mustHexDecodeString(`
|
||||||
|
02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739
|
||||||
|
88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94
|
||||||
|
0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00
|
||||||
|
020304header.go
|
||||||
|
`)
|
||||||
|
|
||||||
|
connID := mustHexDecodeString(`8394c8f03e515708`)
|
||||||
|
|
||||||
|
packet := append([]byte{}, protect...)
|
||||||
|
hdr, offset, err := ParseInitialHeader(packet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version))
|
||||||
|
serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size())
|
||||||
|
key, err := NewInitialProtectionKey(serverSecret, hdr.Version)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pp := NewPacketProtector(key)
|
||||||
|
got, err := pp.UnProtect(protect, offset, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, unProtect) {
|
||||||
|
t.Error("UnProtect returns wrong result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPacketProtectorShortHeader_UnProtect(t *testing.T) {
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea
|
||||||
|
protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`)
|
||||||
|
unProtect := mustHexDecodeString(`01`)
|
||||||
|
hdr := mustHexDecodeString(`4200bff4`)
|
||||||
|
|
||||||
|
secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`)
|
||||||
|
k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pnLen := int(hdr[0]&0x03) + 1
|
||||||
|
offset := len(hdr) - pnLen
|
||||||
|
pp := NewPacketProtector(k)
|
||||||
|
got, err := pp.UnProtect(protect, int64(offset), 654360564)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, unProtect) {
|
||||||
|
t.Error("UnProtect returns wrong result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustHexDecodeString(s string) []byte {
|
||||||
|
b, err := hex.DecodeString(normalizeHex(s))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeHex(s string) string {
|
||||||
|
return strings.Map(func(c rune) rune {
|
||||||
|
if unicode.IsSpace(c) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}, s)
|
||||||
|
}
|
122
analyzer/udp/internal/quic/payload.go
Normal file
122
analyzer/udp/internal/quic/payload.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package quic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/quic-go/quic-go/quicvarint"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReadCryptoPayload(packet []byte) ([]byte, error) {
|
||||||
|
hdr, offset, err := ParseInitialHeader(packet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Some sanity checks
|
||||||
|
if hdr.Version != V1 && hdr.Version != V2 {
|
||||||
|
return nil, fmt.Errorf("unsupported version: %x", hdr.Version)
|
||||||
|
}
|
||||||
|
if offset == 0 || hdr.Length == 0 {
|
||||||
|
return nil, errors.New("invalid packet")
|
||||||
|
}
|
||||||
|
|
||||||
|
initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version))
|
||||||
|
clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size())
|
||||||
|
key, err := NewInitialProtectionKey(clientSecret, hdr.Version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NewInitialProtectionKey: %w", err)
|
||||||
|
}
|
||||||
|
pp := NewPacketProtector(key)
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial
|
||||||
|
//
|
||||||
|
// "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2"
|
||||||
|
if int64(len(packet)) < offset+hdr.Length {
|
||||||
|
return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length)
|
||||||
|
}
|
||||||
|
unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := assembleCryptoFrames(frs)
|
||||||
|
if data == nil {
|
||||||
|
return nil, errors.New("unable to assemble crypto frames")
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
paddingFrameType = 0x00
|
||||||
|
pingFrameType = 0x01
|
||||||
|
cryptoFrameType = 0x06
|
||||||
|
)
|
||||||
|
|
||||||
|
type cryptoFrame struct {
|
||||||
|
Offset int64
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {
|
||||||
|
var frames []cryptoFrame
|
||||||
|
for r.Len() > 0 {
|
||||||
|
typ, err := quicvarint.Read(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if typ == paddingFrameType || typ == pingFrameType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if typ != cryptoFrameType {
|
||||||
|
return nil, fmt.Errorf("encountered unexpected frame type: %d", typ)
|
||||||
|
}
|
||||||
|
var frame cryptoFrame
|
||||||
|
offset, err := quicvarint.Read(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
frame.Offset = int64(offset)
|
||||||
|
dataLen, err := quicvarint.Read(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
frame.Data = make([]byte, dataLen)
|
||||||
|
if _, err := io.ReadFull(r, frame.Data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
return frames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).
|
||||||
|
// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.
|
||||||
|
func assembleCryptoFrames(frames []cryptoFrame) []byte {
|
||||||
|
if len(frames) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(frames) == 1 {
|
||||||
|
return frames[0].Data
|
||||||
|
}
|
||||||
|
// sort the frames by offset
|
||||||
|
sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })
|
||||||
|
// check if the frames are contiguous
|
||||||
|
for i := 1; i < len(frames); i++ {
|
||||||
|
if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// concatenate the frames
|
||||||
|
data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data)))
|
||||||
|
for _, frame := range frames {
|
||||||
|
copy(data[frame.Offset:], frame.Data)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
59
analyzer/udp/internal/quic/quic.go
Normal file
59
analyzer/udp/internal/quic/quic.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package quic
|
||||||
|
|
||||||
|
const (
|
||||||
|
V1 uint32 = 0x1
|
||||||
|
V2 uint32 = 0x6b3343cf
|
||||||
|
|
||||||
|
hkdfLabelKeyV1 = "quic key"
|
||||||
|
hkdfLabelKeyV2 = "quicv2 key"
|
||||||
|
hkdfLabelIVV1 = "quic iv"
|
||||||
|
hkdfLabelIVV2 = "quicv2 iv"
|
||||||
|
hkdfLabelHPV1 = "quic hp"
|
||||||
|
hkdfLabelHPV2 = "quicv2 hp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets
|
||||||
|
quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
|
||||||
|
// https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2
|
||||||
|
quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}
|
||||||
|
)
|
||||||
|
|
||||||
|
// isLongHeader reports whether b is the first byte of a long header packet.
|
||||||
|
func isLongHeader(b byte) bool {
|
||||||
|
return b&0x80 > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSalt(v uint32) []byte {
|
||||||
|
switch v {
|
||||||
|
case V1:
|
||||||
|
return quicSaltV1
|
||||||
|
case V2:
|
||||||
|
return quicSaltV2
|
||||||
|
}
|
||||||
|
return quicSaltOld
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyLabel(v uint32) string {
|
||||||
|
kl := hkdfLabelKeyV1
|
||||||
|
if v == V2 {
|
||||||
|
kl = hkdfLabelKeyV2
|
||||||
|
}
|
||||||
|
return kl
|
||||||
|
}
|
||||||
|
|
||||||
|
func ivLabel(v uint32) string {
|
||||||
|
ivl := hkdfLabelIVV1
|
||||||
|
if v == V2 {
|
||||||
|
ivl = hkdfLabelIVV2
|
||||||
|
}
|
||||||
|
return ivl
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerProtectionLabel(v uint32) string {
|
||||||
|
if v == V2 {
|
||||||
|
return hkdfLabelHPV2
|
||||||
|
}
|
||||||
|
return hkdfLabelHPV1
|
||||||
|
}
|
82
analyzer/udp/quic.go
Normal file
82
analyzer/udp/quic.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package udp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/apernet/OpenGFW/analyzer"
|
||||||
|
"github.com/apernet/OpenGFW/analyzer/internal"
|
||||||
|
"github.com/apernet/OpenGFW/analyzer/udp/internal/quic"
|
||||||
|
"github.com/apernet/OpenGFW/analyzer/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
quicInvalidCountThreshold = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ analyzer.UDPAnalyzer = (*QUICAnalyzer)(nil)
|
||||||
|
_ analyzer.UDPStream = (*quicStream)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type QUICAnalyzer struct{}
|
||||||
|
|
||||||
|
func (a *QUICAnalyzer) Name() string {
|
||||||
|
return "quic"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *QUICAnalyzer) Limit() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *QUICAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
||||||
|
return &quicStream{logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
type quicStream struct {
|
||||||
|
logger analyzer.Logger
|
||||||
|
invalidCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *quicStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
|
||||||
|
if rev {
|
||||||
|
// We don't support server direction for now
|
||||||
|
s.invalidCount++
|
||||||
|
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||||
|
}
|
||||||
|
pl, err := quic.ReadCryptoPayload(data)
|
||||||
|
if err != nil || len(pl) < 4 {
|
||||||
|
s.invalidCount++
|
||||||
|
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||||
|
}
|
||||||
|
// Should be a TLS client hello
|
||||||
|
if pl[0] != 0x01 {
|
||||||
|
// Not a client hello
|
||||||
|
s.invalidCount++
|
||||||
|
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||||
|
}
|
||||||
|
chLen := int(pl[1])<<16 | int(pl[2])<<8 | int(pl[3])
|
||||||
|
if chLen < 41 {
|
||||||
|
// 2 (Protocol Version) +
|
||||||
|
// 32 (Random) +
|
||||||
|
// 1 (Session ID Length) +
|
||||||
|
// 2 (Cipher Suites Length) +_ws.col.protocol == "TLSv1.3"
|
||||||
|
// 2 (Cipher Suite) +
|
||||||
|
// 1 (Compression Methods Length) +
|
||||||
|
// 1 (Compression Method) +
|
||||||
|
// No extensions
|
||||||
|
// This should be the bare minimum for a client hello
|
||||||
|
s.invalidCount++
|
||||||
|
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||||
|
}
|
||||||
|
m := internal.ParseTLSClientHello(&utils.ByteBuffer{Buf: pl[4:]})
|
||||||
|
if m == nil {
|
||||||
|
s.invalidCount++
|
||||||
|
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||||
|
}
|
||||||
|
return &analyzer.PropUpdate{
|
||||||
|
Type: analyzer.PropUpdateMerge,
|
||||||
|
M: analyzer.PropMap{"req": m},
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *quicStream) Close(limited bool) *analyzer.PropUpdate {
|
||||||
|
return nil
|
||||||
|
}
|
@ -93,6 +93,7 @@ var analyzers = []analyzer.Analyzer{
|
|||||||
&tcp.TLSAnalyzer{},
|
&tcp.TLSAnalyzer{},
|
||||||
&tcp.TrojanAnalyzer{},
|
&tcp.TrojanAnalyzer{},
|
||||||
&udp.DNSAnalyzer{},
|
&udp.DNSAnalyzer{},
|
||||||
|
&udp.QUICAnalyzer{},
|
||||||
&udp.WireGuardAnalyzer{},
|
&udp.WireGuardAnalyzer{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
go.mod
6
go.mod
@ -1,6 +1,6 @@
|
|||||||
module github.com/apernet/OpenGFW
|
module github.com/apernet/OpenGFW
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/snowflake v0.3.0
|
github.com/bwmarrin/snowflake v0.3.0
|
||||||
@ -10,10 +10,12 @@ require (
|
|||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
github.com/mdlayher/netlink v1.6.0
|
github.com/mdlayher/netlink v1.6.0
|
||||||
|
github.com/quic-go/quic-go v0.41.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
|
golang.org/x/crypto v0.19.0
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
@ -41,7 +43,7 @@ require (
|
|||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/sync v0.5.0 // indirect
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
)
|
)
|
||||||
|
26
go.sum
26
go.sum
@ -12,8 +12,13 @@ github.com/expr-lang/expr v1.15.7/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf
|
|||||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf h1:NqGS3vTHzVENbIfd87cXZwdpO6MB2R1PjHMJLi4Z3ow=
|
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf h1:NqGS3vTHzVENbIfd87cXZwdpO6MB2R1PjHMJLi4Z3ow=
|
||||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uDjc0EUy0tfq4=
|
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uDjc0EUy0tfq4=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
|
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
@ -22,6 +27,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||||
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
@ -31,7 +38,9 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
|
|||||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
|
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
|
||||||
@ -40,12 +49,19 @@ github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI
|
|||||||
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||||
|
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||||
|
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k=
|
||||||
|
github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
@ -73,12 +89,15 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@ -101,8 +120,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -112,6 +131,8 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@ -120,6 +141,7 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs
|
|||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
Loading…
Reference in New Issue
Block a user