This commit is contained in:
Toby
2024-01-19 16:45:01 -08:00
commit 4f86f91a15
31 changed files with 4397 additions and 0 deletions

131
analyzer/interface.go Normal file
View File

@@ -0,0 +1,131 @@
package analyzer
import (
"net"
"strings"
)
type Analyzer interface {
// Name returns the name of the analyzer.
Name() string
// Limit returns the byte limit for this analyzer.
// For example, an analyzer can return 1000 to indicate that it only ever needs
// the first 1000 bytes of a stream to do its job. If the stream is still not
// done after 1000 bytes, the engine will stop feeding it data and close it.
// An analyzer can return 0 or a negative number to indicate that it does not
// have a hard limit.
// Note: for UDP streams, the engine always feeds entire packets, even if
// the packet is larger than the remaining quota or the limit itself.
Limit() int
}
type Logger interface {
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Errorf(format string, args ...interface{})
}
type TCPAnalyzer interface {
Analyzer
// NewTCP returns a new TCPStream.
NewTCP(TCPInfo, Logger) TCPStream
}
type TCPInfo struct {
// SrcIP is the source IP address.
SrcIP net.IP
// DstIP is the destination IP address.
DstIP net.IP
// SrcPort is the source port.
SrcPort uint16
// DstPort is the destination port.
DstPort uint16
}
type TCPStream interface {
// Feed feeds a chunk of reassembled data to the stream.
// It returns a prop update containing the information extracted from the stream (can be nil),
// and whether the analyzer is "done" with this stream (i.e. no more data should be fed).
Feed(rev, start, end bool, skip int, data []byte) (u *PropUpdate, done bool)
// Close indicates that the stream is closed.
// Either the connection is closed, or the stream has reached its byte limit.
// Like Feed, it optionally returns a prop update.
Close(limited bool) *PropUpdate
}
type UDPAnalyzer interface {
Analyzer
// NewUDP returns a new UDPStream.
NewUDP(UDPInfo, Logger) UDPStream
}
type UDPInfo struct {
// SrcIP is the source IP address.
SrcIP net.IP
// DstIP is the destination IP address.
DstIP net.IP
// SrcPort is the source port.
SrcPort uint16
// DstPort is the destination port.
DstPort uint16
}
type UDPStream interface {
// Feed feeds a new packet to the stream.
// It returns a prop update containing the information extracted from the stream (can be nil),
// and whether the analyzer is "done" with this stream (i.e. no more data should be fed).
Feed(rev bool, data []byte) (u *PropUpdate, done bool)
// Close indicates that the stream is closed.
// Either the connection is closed, or the stream has reached its byte limit.
// Like Feed, it optionally returns a prop update.
Close(limited bool) *PropUpdate
}
type (
PropMap map[string]interface{}
CombinedPropMap map[string]PropMap
)
// Get returns the value of the property with the given key.
// The key can be a nested key, e.g. "foo.bar.baz".
// Returns nil if the key does not exist.
func (m PropMap) Get(key string) interface{} {
keys := strings.Split(key, ".")
if len(keys) == 0 {
return nil
}
var current interface{} = m
for _, k := range keys {
currentMap, ok := current.(PropMap)
if !ok {
return nil
}
current = currentMap[k]
}
return current
}
// Get returns the value of the property with the given analyzer & key.
// The key can be a nested key, e.g. "foo.bar.baz".
// Returns nil if the key does not exist.
func (cm CombinedPropMap) Get(an string, key string) interface{} {
m, ok := cm[an]
if !ok {
return nil
}
return m.Get(key)
}
type PropUpdateType int
const (
PropUpdateNone PropUpdateType = iota
PropUpdateMerge
PropUpdateReplace
PropUpdateDelete
)
type PropUpdate struct {
Type PropUpdateType
M PropMap
}

159
analyzer/tcp/fet.go Normal file
View File

@@ -0,0 +1,159 @@
package tcp
import "github.com/apernet/OpenGFW/analyzer"
var _ analyzer.TCPAnalyzer = (*FETAnalyzer)(nil)
// FETAnalyzer stands for "Fully Encrypted Traffic" analyzer.
// It implements an algorithm to detect fully encrypted proxy protocols
// such as Shadowsocks, mentioned in the following paper:
// https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf
type FETAnalyzer struct{}
func (a *FETAnalyzer) Name() string {
return "fet"
}
func (a *FETAnalyzer) Limit() int {
// We only really look at the first packet
return 8192
}
func (a *FETAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newFETStream(logger)
}
type fetStream struct {
logger analyzer.Logger
}
func newFETStream(logger analyzer.Logger) *fetStream {
return &fetStream{logger: logger}
}
func (s *fetStream) 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
}
ex1 := averagePopCount(data)
ex2 := isFirstSixPrintable(data)
ex3 := printablePercentage(data)
ex4 := contiguousPrintable(data)
ex5 := isTLSorHTTP(data)
exempt := (ex1 <= 3.4 || ex1 >= 4.6) || ex2 || ex3 > 0.5 || ex4 > 20 || ex5
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{
"ex1": ex1,
"ex2": ex2,
"ex3": ex3,
"ex4": ex4,
"ex5": ex5,
"yes": !exempt,
},
}, true
}
func (s *fetStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
func popCount(b byte) int {
count := 0
for b != 0 {
count += int(b & 1)
b >>= 1
}
return count
}
// averagePopCount returns the average popcount of the given bytes.
// This is the "Ex1" metric in the paper.
func averagePopCount(bytes []byte) float32 {
if len(bytes) == 0 {
return 0
}
total := 0
for _, b := range bytes {
total += popCount(b)
}
return float32(total) / float32(len(bytes))
}
// isFirstSixPrintable returns true if the first six bytes are printable ASCII.
// This is the "Ex2" metric in the paper.
func isFirstSixPrintable(bytes []byte) bool {
if len(bytes) < 6 {
return false
}
for i := range bytes[:6] {
if !isPrintable(bytes[i]) {
return false
}
}
return true
}
// printablePercentage returns the percentage of printable ASCII bytes.
// This is the "Ex3" metric in the paper.
func printablePercentage(bytes []byte) float32 {
if len(bytes) == 0 {
return 0
}
count := 0
for i := range bytes {
if isPrintable(bytes[i]) {
count++
}
}
return float32(count) / float32(len(bytes))
}
// contiguousPrintable returns the length of the longest contiguous sequence of
// printable ASCII bytes.
// This is the "Ex4" metric in the paper.
func contiguousPrintable(bytes []byte) int {
if len(bytes) == 0 {
return 0
}
maxCount := 0
current := 0
for i := range bytes {
if isPrintable(bytes[i]) {
current++
} else {
if current > maxCount {
maxCount = current
}
current = 0
}
}
if current > maxCount {
maxCount = current
}
return maxCount
}
// isTLSorHTTP returns true if the given bytes look like TLS or HTTP.
// This is the "Ex5" metric in the paper.
func isTLSorHTTP(bytes []byte) bool {
if len(bytes) < 3 {
return false
}
if bytes[0] == 0x16 && bytes[1] == 0x03 && bytes[2] <= 0x03 {
// TLS handshake for TLS 1.0-1.3
return true
}
// HTTP request
str := string(bytes[:3])
return str == "GET" || str == "HEA" || str == "POS" ||
str == "PUT" || str == "DEL" || str == "CON" ||
str == "OPT" || str == "TRA" || str == "PAT"
}
func isPrintable(b byte) bool {
return b >= 0x20 && b <= 0x7e
}

193
analyzer/tcp/http.go Normal file
View File

@@ -0,0 +1,193 @@
package tcp
import (
"bytes"
"strconv"
"strings"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
var _ analyzer.TCPAnalyzer = (*HTTPAnalyzer)(nil)
type HTTPAnalyzer struct{}
func (a *HTTPAnalyzer) Name() string {
return "http"
}
func (a *HTTPAnalyzer) Limit() int {
return 8192
}
func (a *HTTPAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newHTTPStream(logger)
}
type httpStream 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
}
func newHTTPStream(logger analyzer.Logger) *httpStream {
s := &httpStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
s.reqLSM = utils.NewLinearStateMachine(
s.parseRequestLine,
s.parseRequestHeaders,
)
s.respLSM = utils.NewLinearStateMachine(
s.parseResponseLine,
s.parseResponseHeaders,
)
return s
}
func (s *httpStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, d 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 *httpStream) parseRequestLine() utils.LSMAction {
// Find the end of the request line
line, ok := s.reqBuf.GetUntil([]byte("\r\n"), true, true)
if !ok {
// No end of line yet, but maybe we just need more data
return utils.LSMActionPause
}
fields := strings.Fields(string(line[:len(line)-2])) // Strip \r\n
if len(fields) != 3 {
// Invalid request line
return utils.LSMActionCancel
}
method := fields[0]
path := fields[1]
version := fields[2]
if !strings.HasPrefix(version, "HTTP/") {
// Invalid version
return utils.LSMActionCancel
}
s.reqMap = analyzer.PropMap{
"method": method,
"path": path,
"version": version,
}
s.reqUpdated = true
return utils.LSMActionNext
}
func (s *httpStream) parseResponseLine() utils.LSMAction {
// Find the end of the response line
line, ok := s.respBuf.GetUntil([]byte("\r\n"), true, true)
if !ok {
// No end of line yet, but maybe we just need more data
return utils.LSMActionPause
}
fields := strings.Fields(string(line[:len(line)-2])) // Strip \r\n
if len(fields) < 2 {
// Invalid response line
return utils.LSMActionCancel
}
version := fields[0]
status, _ := strconv.Atoi(fields[1])
if !strings.HasPrefix(version, "HTTP/") || status == 0 {
// Invalid version
return utils.LSMActionCancel
}
s.respMap = analyzer.PropMap{
"version": version,
"status": status,
}
s.respUpdated = true
return utils.LSMActionNext
}
func (s *httpStream) parseHeaders(buf *utils.ByteBuffer) (utils.LSMAction, analyzer.PropMap) {
// Find the end of headers
headers, ok := buf.GetUntil([]byte("\r\n\r\n"), true, true)
if !ok {
// No end of headers yet, but maybe we just need more data
return utils.LSMActionPause, nil
}
headers = headers[:len(headers)-4] // Strip \r\n\r\n
headerMap := make(analyzer.PropMap)
for _, line := range bytes.Split(headers, []byte("\r\n")) {
fields := bytes.SplitN(line, []byte(":"), 2)
if len(fields) != 2 {
// Invalid header
return utils.LSMActionCancel, nil
}
key := string(bytes.TrimSpace(fields[0]))
value := string(bytes.TrimSpace(fields[1]))
// Normalize header keys to lowercase
headerMap[strings.ToLower(key)] = value
}
return utils.LSMActionNext, headerMap
}
func (s *httpStream) parseRequestHeaders() utils.LSMAction {
action, headerMap := s.parseHeaders(s.reqBuf)
if action == utils.LSMActionNext {
s.reqMap["headers"] = headerMap
s.reqUpdated = true
}
return action
}
func (s *httpStream) parseResponseHeaders() utils.LSMAction {
action, headerMap := s.parseHeaders(s.respBuf)
if action == utils.LSMActionNext {
s.respMap["headers"] = headerMap
s.respUpdated = true
}
return action
}
func (s *httpStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
s.respBuf.Reset()
s.reqMap = nil
s.respMap = nil
return nil
}

147
analyzer/tcp/ssh.go Normal file
View File

@@ -0,0 +1,147 @@
package tcp
import (
"strings"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
var _ analyzer.TCPAnalyzer = (*SSHAnalyzer)(nil)
type SSHAnalyzer struct{}
func (a *SSHAnalyzer) Name() string {
return "ssh"
}
func (a *SSHAnalyzer) Limit() int {
return 1024
}
func (a *SSHAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newSSHStream(logger)
}
type sshStream struct {
logger analyzer.Logger
clientBuf *utils.ByteBuffer
clientMap analyzer.PropMap
clientUpdated bool
clientLSM *utils.LinearStateMachine
clientDone bool
serverBuf *utils.ByteBuffer
serverMap analyzer.PropMap
serverUpdated bool
serverLSM *utils.LinearStateMachine
serverDone bool
}
func newSSHStream(logger analyzer.Logger) *sshStream {
s := &sshStream{logger: logger, clientBuf: &utils.ByteBuffer{}, serverBuf: &utils.ByteBuffer{}}
s.clientLSM = utils.NewLinearStateMachine(
s.parseClientExchangeLine,
)
s.serverLSM = utils.NewLinearStateMachine(
s.parseServerExchangeLine,
)
return s
}
func (s *sshStream) 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.serverBuf.Append(data)
s.serverUpdated = false
cancelled, s.serverDone = s.serverLSM.Run()
if s.serverUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"server": s.serverMap},
}
s.serverUpdated = false
}
} else {
s.clientBuf.Append(data)
s.clientUpdated = false
cancelled, s.clientDone = s.clientLSM.Run()
if s.clientUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"client": s.clientMap},
}
s.clientUpdated = false
}
}
return update, cancelled || (s.clientDone && s.serverDone)
}
// parseExchangeLine parses the SSH Protocol Version Exchange string.
// See RFC 4253, section 4.2.
// "SSH-protoversion-softwareversion SP comments CR LF"
// The "comments" part (along with the SP) is optional.
func (s *sshStream) parseExchangeLine(buf *utils.ByteBuffer) (utils.LSMAction, analyzer.PropMap) {
// Find the end of the line
line, ok := buf.GetUntil([]byte("\r\n"), true, true)
if !ok {
// No end of line yet, but maybe we just need more data
return utils.LSMActionPause, nil
}
if !strings.HasPrefix(string(line), "SSH-") {
// Not SSH
return utils.LSMActionCancel, nil
}
fields := strings.Fields(string(line[:len(line)-2])) // Strip \r\n
if len(fields) < 1 || len(fields) > 2 {
// Invalid line
return utils.LSMActionCancel, nil
}
sshFields := strings.SplitN(fields[0], "-", 3)
if len(sshFields) != 3 {
// Invalid SSH version format
return utils.LSMActionCancel, nil
}
sMap := analyzer.PropMap{
"protocol": sshFields[1],
"software": sshFields[2],
}
if len(fields) == 2 {
sMap["comments"] = fields[1]
}
return utils.LSMActionNext, sMap
}
func (s *sshStream) parseClientExchangeLine() utils.LSMAction {
action, sMap := s.parseExchangeLine(s.clientBuf)
if action == utils.LSMActionNext {
s.clientMap = sMap
s.clientUpdated = true
}
return action
}
func (s *sshStream) parseServerExchangeLine() utils.LSMAction {
action, sMap := s.parseExchangeLine(s.serverBuf)
if action == utils.LSMActionNext {
s.serverMap = sMap
s.serverUpdated = true
}
return action
}
func (s *sshStream) Close(limited bool) *analyzer.PropUpdate {
s.clientBuf.Reset()
s.serverBuf.Reset()
s.clientMap = nil
s.serverMap = nil
return nil
}

354
analyzer/tcp/tls.go Normal file
View File

@@ -0,0 +1,354 @@
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
}

254
analyzer/udp/dns.go Normal file
View File

@@ -0,0 +1,254 @@
package udp
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// DNSAnalyzer is for both DNS over UDP and TCP.
var (
_ analyzer.UDPAnalyzer = (*DNSAnalyzer)(nil)
_ analyzer.TCPAnalyzer = (*DNSAnalyzer)(nil)
)
type DNSAnalyzer struct{}
func (a *DNSAnalyzer) Name() string {
return "dns"
}
func (a *DNSAnalyzer) Limit() int {
// DNS is a stateless protocol, with unlimited amount
// of back-and-forth exchanges. Don't limit it here.
return 0
}
func (a *DNSAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
return &dnsUDPStream{logger: logger}
}
func (a *DNSAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
s := &dnsTCPStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
s.reqLSM = utils.NewLinearStateMachine(
s.getReqMessageLength,
s.getReqMessage,
)
s.respLSM = utils.NewLinearStateMachine(
s.getRespMessageLength,
s.getRespMessage,
)
return s
}
type dnsUDPStream struct {
logger analyzer.Logger
}
func (s *dnsUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
m := parseDNSMessage(data)
if m == nil {
return nil, false
}
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: m,
}, false
}
func (s *dnsUDPStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
type dnsTCPStream 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
reqMsgLen int
respMsgLen int
}
func (s *dnsTCPStream) 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.PropUpdateReplace,
M: 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.PropUpdateReplace,
M: s.reqMap,
}
s.reqUpdated = false
}
}
return update, cancelled || (s.reqDone && s.respDone)
}
func (s *dnsTCPStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
s.respBuf.Reset()
s.reqMap = nil
s.respMap = nil
return nil
}
func (s *dnsTCPStream) getReqMessageLength() utils.LSMAction {
bs, ok := s.reqBuf.Get(2, true)
if !ok {
return utils.LSMActionPause
}
s.reqMsgLen = int(bs[0])<<8 | int(bs[1])
return utils.LSMActionNext
}
func (s *dnsTCPStream) getRespMessageLength() utils.LSMAction {
bs, ok := s.respBuf.Get(2, true)
if !ok {
return utils.LSMActionPause
}
s.respMsgLen = int(bs[0])<<8 | int(bs[1])
return utils.LSMActionNext
}
func (s *dnsTCPStream) getReqMessage() utils.LSMAction {
bs, ok := s.reqBuf.Get(s.reqMsgLen, true)
if !ok {
return utils.LSMActionPause
}
m := parseDNSMessage(bs)
if m == nil {
// Invalid DNS message
return utils.LSMActionCancel
}
s.reqMap = m
s.reqUpdated = true
return utils.LSMActionReset
}
func (s *dnsTCPStream) getRespMessage() utils.LSMAction {
bs, ok := s.respBuf.Get(s.respMsgLen, true)
if !ok {
return utils.LSMActionPause
}
m := parseDNSMessage(bs)
if m == nil {
// Invalid DNS message
return utils.LSMActionCancel
}
s.respMap = m
s.respUpdated = true
return utils.LSMActionReset
}
func parseDNSMessage(msg []byte) analyzer.PropMap {
dns := &layers.DNS{}
err := dns.DecodeFromBytes(msg, gopacket.NilDecodeFeedback)
if err != nil {
// Not a DNS packet
return nil
}
m := analyzer.PropMap{
"id": dns.ID,
"qr": dns.QR,
"opcode": dns.OpCode,
"aa": dns.AA,
"tc": dns.TC,
"rd": dns.RD,
"ra": dns.RA,
"z": dns.Z,
"rcode": dns.ResponseCode,
}
if len(dns.Questions) > 0 {
mQuestions := make([]analyzer.PropMap, len(dns.Questions))
for i, q := range dns.Questions {
mQuestions[i] = analyzer.PropMap{
"name": string(q.Name),
"type": q.Type,
"class": q.Class,
}
}
m["questions"] = mQuestions
}
if len(dns.Answers) > 0 {
mAnswers := make([]analyzer.PropMap, len(dns.Answers))
for i, rr := range dns.Answers {
mAnswers[i] = dnsRRToPropMap(rr)
}
m["answers"] = mAnswers
}
if len(dns.Authorities) > 0 {
mAuthorities := make([]analyzer.PropMap, len(dns.Authorities))
for i, rr := range dns.Authorities {
mAuthorities[i] = dnsRRToPropMap(rr)
}
m["authorities"] = mAuthorities
}
if len(dns.Additionals) > 0 {
mAdditionals := make([]analyzer.PropMap, len(dns.Additionals))
for i, rr := range dns.Additionals {
mAdditionals[i] = dnsRRToPropMap(rr)
}
m["additionals"] = mAdditionals
}
return m
}
func dnsRRToPropMap(rr layers.DNSResourceRecord) analyzer.PropMap {
m := analyzer.PropMap{
"name": string(rr.Name),
"type": rr.Type,
"class": rr.Class,
"ttl": rr.TTL,
}
switch rr.Type {
// These are not everything, but is
// all we decided to support for now.
case layers.DNSTypeA:
m["a"] = rr.IP.String()
case layers.DNSTypeAAAA:
m["aaaa"] = rr.IP.String()
case layers.DNSTypeNS:
m["ns"] = string(rr.NS)
case layers.DNSTypeCNAME:
m["cname"] = string(rr.CNAME)
case layers.DNSTypePTR:
m["ptr"] = string(rr.PTR)
case layers.DNSTypeTXT:
m["txt"] = utils.ByteSlicesToStrings(rr.TXTs)
case layers.DNSTypeMX:
m["mx"] = string(rr.MX.Name)
}
return m
}

View File

@@ -0,0 +1,99 @@
package utils
import "bytes"
type ByteBuffer struct {
Buf []byte
}
func (b *ByteBuffer) Append(data []byte) {
b.Buf = append(b.Buf, data...)
}
func (b *ByteBuffer) Len() int {
return len(b.Buf)
}
func (b *ByteBuffer) Index(sep []byte) int {
return bytes.Index(b.Buf, sep)
}
func (b *ByteBuffer) Get(length int, consume bool) (data []byte, ok bool) {
if len(b.Buf) < length {
return nil, false
}
data = b.Buf[:length]
if consume {
b.Buf = b.Buf[length:]
}
return data, true
}
func (b *ByteBuffer) GetString(length int, consume bool) (string, bool) {
data, ok := b.Get(length, consume)
if !ok {
return "", false
}
return string(data), true
}
func (b *ByteBuffer) GetByte(consume bool) (byte, bool) {
data, ok := b.Get(1, consume)
if !ok {
return 0, false
}
return data[0], true
}
func (b *ByteBuffer) GetUint16(littleEndian, consume bool) (uint16, bool) {
data, ok := b.Get(2, consume)
if !ok {
return 0, false
}
if littleEndian {
return uint16(data[0]) | uint16(data[1])<<8, true
}
return uint16(data[1]) | uint16(data[0])<<8, true
}
func (b *ByteBuffer) GetUint32(littleEndian, consume bool) (uint32, bool) {
data, ok := b.Get(4, consume)
if !ok {
return 0, false
}
if littleEndian {
return uint32(data[0]) | uint32(data[1])<<8 | uint32(data[2])<<16 | uint32(data[3])<<24, true
}
return uint32(data[3]) | uint32(data[2])<<8 | uint32(data[1])<<16 | uint32(data[0])<<24, true
}
func (b *ByteBuffer) GetUntil(sep []byte, includeSep, consume bool) (data []byte, ok bool) {
index := b.Index(sep)
if index == -1 {
return nil, false
}
if includeSep {
index += len(sep)
}
return b.Get(index, consume)
}
func (b *ByteBuffer) GetSubBuffer(length int, consume bool) (sub *ByteBuffer, ok bool) {
data, ok := b.Get(length, consume)
if !ok {
return nil, false
}
return &ByteBuffer{Buf: data}, true
}
func (b *ByteBuffer) Skip(length int) bool {
if len(b.Buf) < length {
return false
}
b.Buf = b.Buf[length:]
return true
}
func (b *ByteBuffer) Reset() {
b.Buf = nil
}

50
analyzer/utils/lsm.go Normal file
View File

@@ -0,0 +1,50 @@
package utils
type LSMAction int
const (
LSMActionPause LSMAction = iota
LSMActionNext
LSMActionReset
LSMActionCancel
)
type LinearStateMachine struct {
Steps []func() LSMAction
index int
cancelled bool
}
func NewLinearStateMachine(steps ...func() LSMAction) *LinearStateMachine {
return &LinearStateMachine{
Steps: steps,
}
}
// Run runs the state machine until it pauses, finishes or is cancelled.
func (lsm *LinearStateMachine) Run() (cancelled bool, done bool) {
if lsm.index >= len(lsm.Steps) {
return lsm.cancelled, true
}
for lsm.index < len(lsm.Steps) {
action := lsm.Steps[lsm.index]()
switch action {
case LSMActionPause:
return false, false
case LSMActionNext:
lsm.index++
case LSMActionReset:
lsm.index = 0
case LSMActionCancel:
lsm.cancelled = true
return true, true
}
}
return false, true
}
func (lsm *LinearStateMachine) Reset() {
lsm.index = 0
lsm.cancelled = false
}

9
analyzer/utils/string.go Normal file
View File

@@ -0,0 +1,9 @@
package utils
func ByteSlicesToStrings(bss [][]byte) []string {
ss := make([]string, len(bss))
for i, bs := range bss {
ss[i] = string(bs)
}
return ss
}