mirror of
https://github.com/apernet/OpenGFW.git
synced 2024-11-14 22:39:26 +08:00
355 lines
9.3 KiB
Go
355 lines
9.3 KiB
Go
|
package tcp
|
||
|
|
||
|
import (
|
||
|
"github.com/apernet/OpenGFW/analyzer"
|
||
|
"github.com/apernet/OpenGFW/analyzer/utils"
|
||
|
)
|
||
|
|
||
|
var _ analyzer.TCPAnalyzer = (*TLSAnalyzer)(nil)
|
||
|
|
||
|
type TLSAnalyzer struct{}
|
||
|
|
||
|
func (a *TLSAnalyzer) Name() string {
|
||
|
return "tls"
|
||
|
}
|
||
|
|
||
|
func (a *TLSAnalyzer) Limit() int {
|
||
|
return 8192
|
||
|
}
|
||
|
|
||
|
func (a *TLSAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
|
||
|
return newTLSStream(logger)
|
||
|
}
|
||
|
|
||
|
type tlsStream struct {
|
||
|
logger analyzer.Logger
|
||
|
|
||
|
reqBuf *utils.ByteBuffer
|
||
|
reqMap analyzer.PropMap
|
||
|
reqUpdated bool
|
||
|
reqLSM *utils.LinearStateMachine
|
||
|
reqDone bool
|
||
|
|
||
|
respBuf *utils.ByteBuffer
|
||
|
respMap analyzer.PropMap
|
||
|
respUpdated bool
|
||
|
respLSM *utils.LinearStateMachine
|
||
|
respDone bool
|
||
|
|
||
|
clientHelloLen int
|
||
|
serverHelloLen int
|
||
|
}
|
||
|
|
||
|
func newTLSStream(logger analyzer.Logger) *tlsStream {
|
||
|
s := &tlsStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
|
||
|
s.reqLSM = utils.NewLinearStateMachine(
|
||
|
s.tlsClientHelloSanityCheck,
|
||
|
s.parseClientHello,
|
||
|
)
|
||
|
s.respLSM = utils.NewLinearStateMachine(
|
||
|
s.tlsServerHelloSanityCheck,
|
||
|
s.parseServerHello,
|
||
|
)
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
func (s *tlsStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
|
||
|
if skip != 0 {
|
||
|
return nil, true
|
||
|
}
|
||
|
if len(data) == 0 {
|
||
|
return nil, false
|
||
|
}
|
||
|
var update *analyzer.PropUpdate
|
||
|
var cancelled bool
|
||
|
if rev {
|
||
|
s.respBuf.Append(data)
|
||
|
s.respUpdated = false
|
||
|
cancelled, s.respDone = s.respLSM.Run()
|
||
|
if s.respUpdated {
|
||
|
update = &analyzer.PropUpdate{
|
||
|
Type: analyzer.PropUpdateMerge,
|
||
|
M: analyzer.PropMap{"resp": s.respMap},
|
||
|
}
|
||
|
s.respUpdated = false
|
||
|
}
|
||
|
} else {
|
||
|
s.reqBuf.Append(data)
|
||
|
s.reqUpdated = false
|
||
|
cancelled, s.reqDone = s.reqLSM.Run()
|
||
|
if s.reqUpdated {
|
||
|
update = &analyzer.PropUpdate{
|
||
|
Type: analyzer.PropUpdateMerge,
|
||
|
M: analyzer.PropMap{"req": s.reqMap},
|
||
|
}
|
||
|
s.reqUpdated = false
|
||
|
}
|
||
|
}
|
||
|
return update, cancelled || (s.reqDone && s.respDone)
|
||
|
}
|
||
|
|
||
|
func (s *tlsStream) tlsClientHelloSanityCheck() utils.LSMAction {
|
||
|
data, ok := s.reqBuf.Get(9, true)
|
||
|
if !ok {
|
||
|
return utils.LSMActionPause
|
||
|
}
|
||
|
if data[0] != 0x16 || data[5] != 0x01 {
|
||
|
// Not a TLS handshake, or not a client hello
|
||
|
return utils.LSMActionCancel
|
||
|
}
|
||
|
s.clientHelloLen = int(data[6])<<16 | int(data[7])<<8 | int(data[8])
|
||
|
if s.clientHelloLen < 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
|
||
|
return utils.LSMActionCancel
|
||
|
}
|
||
|
return utils.LSMActionNext
|
||
|
}
|
||
|
|
||
|
func (s *tlsStream) tlsServerHelloSanityCheck() utils.LSMAction {
|
||
|
data, ok := s.respBuf.Get(9, true)
|
||
|
if !ok {
|
||
|
return utils.LSMActionPause
|
||
|
}
|
||
|
if data[0] != 0x16 || data[5] != 0x02 {
|
||
|
// Not a TLS handshake, or not a server hello
|
||
|
return utils.LSMActionCancel
|
||
|
}
|
||
|
s.serverHelloLen = int(data[6])<<16 | int(data[7])<<8 | int(data[8])
|
||
|
if s.serverHelloLen < 38 {
|
||
|
// 2 (Protocol Version) +
|
||
|
// 32 (Random) +
|
||
|
// 1 (Session ID Length) +
|
||
|
// 2 (Cipher Suite) +
|
||
|
// 1 (Compression Method) +
|
||
|
// No extensions
|
||
|
// This should be the bare minimum for a server hello
|
||
|
return utils.LSMActionCancel
|
||
|
}
|
||
|
return utils.LSMActionNext
|
||
|
}
|
||
|
|
||
|
func (s *tlsStream) parseClientHello() utils.LSMAction {
|
||
|
chBuf, ok := s.reqBuf.GetSubBuffer(s.clientHelloLen, true)
|
||
|
if !ok {
|
||
|
// Not a full client hello yet
|
||
|
return utils.LSMActionPause
|
||
|
}
|
||
|
s.reqUpdated = true
|
||
|
s.reqMap = make(analyzer.PropMap)
|
||
|
// 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
|
||
|
}
|
||
|
cipherSuitesLen, ok := chBuf.GetUint16(false, true)
|
||
|
if !ok {
|
||
|
// 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
|
||
|
}
|
||
|
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 {
|
||
|
shBuf, ok := s.respBuf.GetSubBuffer(s.serverHelloLen, true)
|
||
|
if !ok {
|
||
|
// Not a full server hello yet
|
||
|
return utils.LSMActionPause
|
||
|
}
|
||
|
s.respUpdated = true
|
||
|
s.respMap = make(analyzer.PropMap)
|
||
|
// 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
|
||
|
}
|
||
|
cipherSuite, ok := shBuf.GetUint16(false, true)
|
||
|
if !ok {
|
||
|
// 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
|
||
|
}
|
||
|
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 {
|
||
|
s.reqBuf.Reset()
|
||
|
s.respBuf.Reset()
|
||
|
s.reqMap = nil
|
||
|
s.respMap = nil
|
||
|
return nil
|
||
|
}
|