mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
修复协议屏蔽
This commit is contained in:
+5
-2
@@ -8,8 +8,11 @@ import (
|
|||||||
|
|
||||||
// Config 配置结构体
|
// Config 配置结构体
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Addr string `json:"addr"` // WebSocket地址
|
Addr string `json:"addr"`
|
||||||
Secret string `json:"secret"` // 密钥
|
Secret string `json:"secret"`
|
||||||
|
Http int `json:"http"`
|
||||||
|
Tls int `json:"tls"`
|
||||||
|
Socks int `json:"socks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig 加载配置文件
|
// LoadConfig 加载配置文件
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TLS record types.
|
|
||||||
const (
|
|
||||||
RecordTypeHandshake = 0x16
|
|
||||||
)
|
|
||||||
|
|
||||||
// TLS handshake message types.
|
|
||||||
const (
|
|
||||||
TypeClientHello = 0x01
|
|
||||||
TypeServerHello = 0x02
|
|
||||||
)
|
|
||||||
|
|
||||||
// TLS extension numbers.
|
|
||||||
const (
|
|
||||||
extServerName = 0x0000
|
|
||||||
extALPN = 0x0010
|
|
||||||
extSupportedVersions = 0x002b
|
|
||||||
extEncryptedClientHello = 0xfe0d
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseTLSClientHelloMsgData(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 ParseTLSServerHelloMsgData(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 extServerName:
|
|
||||||
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 extALPN:
|
|
||||||
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 extSupportedVersions:
|
|
||||||
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 extEncryptedClientHello:
|
|
||||||
// We can't parse ECH for now, just set a flag
|
|
||||||
m["ech"] = true
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
// "We observe that the GFW exempts any connection whose first
|
|
||||||
// three bytes match the following regular expression:
|
|
||||||
// [\x16-\x17]\x03[\x00-\x09]" - from the paper in Section 4.3
|
|
||||||
if bytes[0] >= 0x16 && bytes[0] <= 0x17 &&
|
|
||||||
bytes[1] == 0x03 && bytes[2] <= 0x09 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,508 +0,0 @@
|
|||||||
package tcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SocksInvalid = iota
|
|
||||||
Socks4
|
|
||||||
Socks4A
|
|
||||||
Socks5
|
|
||||||
|
|
||||||
Socks4Version = 0x04
|
|
||||||
Socks5Version = 0x05
|
|
||||||
|
|
||||||
Socks4ReplyVN = 0x00
|
|
||||||
|
|
||||||
Socks4CmdTCPConnect = 0x01
|
|
||||||
Socks4CmdTCPBind = 0x02
|
|
||||||
|
|
||||||
Socks4ReqGranted = 0x5A
|
|
||||||
Socks4ReqRejectOrFailed = 0x5B
|
|
||||||
Socks4ReqRejectIdentd = 0x5C
|
|
||||||
Socks4ReqRejectUser = 0x5D
|
|
||||||
|
|
||||||
Socks5CmdTCPConnect = 0x01
|
|
||||||
Socks5CmdTCPBind = 0x02
|
|
||||||
Socks5CmdUDPAssociate = 0x03
|
|
||||||
|
|
||||||
Socks5AuthNotRequired = 0x00
|
|
||||||
Socks5AuthPassword = 0x02
|
|
||||||
Socks5AuthNoMatchingMethod = 0xFF
|
|
||||||
|
|
||||||
Socks5AuthSuccess = 0x00
|
|
||||||
Socks5AuthFailure = 0x01
|
|
||||||
|
|
||||||
Socks5AddrTypeIPv4 = 0x01
|
|
||||||
Socks5AddrTypeDomain = 0x03
|
|
||||||
Socks5AddrTypeIPv6 = 0x04
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ analyzer.Analyzer = (*SocksAnalyzer)(nil)
|
|
||||||
|
|
||||||
type SocksAnalyzer struct{}
|
|
||||||
|
|
||||||
func (a *SocksAnalyzer) Name() string {
|
|
||||||
return "socks"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *SocksAnalyzer) Limit() int {
|
|
||||||
// Socks4 length limit cannot be predicted
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *SocksAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
|
|
||||||
return newSocksStream(logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
type socksStream 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
|
|
||||||
|
|
||||||
version int
|
|
||||||
|
|
||||||
authReqMethod int
|
|
||||||
authUsername string
|
|
||||||
authPassword string
|
|
||||||
|
|
||||||
authRespMethod int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSocksStream(logger analyzer.Logger) *socksStream {
|
|
||||||
s := &socksStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
|
|
||||||
s.reqLSM = utils.NewLinearStateMachine(
|
|
||||||
s.parseSocksReqVersion,
|
|
||||||
)
|
|
||||||
s.respLSM = utils.NewLinearStateMachine(
|
|
||||||
s.parseSocksRespVersion,
|
|
||||||
)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) 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{
|
|
||||||
"version": s.socksVersion(),
|
|
||||||
"req": s.reqMap,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
s.reqUpdated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return update, cancelled || (s.reqDone && s.respDone)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) Close(limited bool) *analyzer.PropUpdate {
|
|
||||||
s.reqBuf.Reset()
|
|
||||||
s.respBuf.Reset()
|
|
||||||
s.reqMap = nil
|
|
||||||
s.respMap = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocksReqVersion() utils.LSMAction {
|
|
||||||
socksVer, ok := s.reqBuf.GetByte(true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
if socksVer != Socks4Version && socksVer != Socks5Version {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.reqMap = make(analyzer.PropMap)
|
|
||||||
s.reqUpdated = true
|
|
||||||
if socksVer == Socks4Version {
|
|
||||||
s.version = Socks4
|
|
||||||
s.reqLSM.AppendSteps(
|
|
||||||
s.parseSocks4ReqIpAndPort,
|
|
||||||
s.parseSocks4ReqUserId,
|
|
||||||
s.parseSocks4ReqHostname,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
s.version = Socks5
|
|
||||||
s.reqLSM.AppendSteps(
|
|
||||||
s.parseSocks5ReqMethod,
|
|
||||||
s.parseSocks5ReqAuth,
|
|
||||||
s.parseSocks5ReqConnInfo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocksRespVersion() utils.LSMAction {
|
|
||||||
socksVer, ok := s.respBuf.GetByte(true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
if (s.version == Socks4 || s.version == Socks4A) && socksVer != Socks4ReplyVN ||
|
|
||||||
s.version == Socks5 && socksVer != Socks5Version || s.version == SocksInvalid {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
if socksVer == Socks4ReplyVN {
|
|
||||||
s.respLSM.AppendSteps(
|
|
||||||
s.parseSocks4RespPacket,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
s.respLSM.AppendSteps(
|
|
||||||
s.parseSocks5RespMethod,
|
|
||||||
s.parseSocks5RespAuth,
|
|
||||||
s.parseSocks5RespConnInfo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks5ReqMethod() utils.LSMAction {
|
|
||||||
nMethods, ok := s.reqBuf.GetByte(false)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
methods, ok := s.reqBuf.Get(int(nMethods)+1, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
// For convenience, we only take the first method we can process
|
|
||||||
s.authReqMethod = Socks5AuthNoMatchingMethod
|
|
||||||
for _, method := range methods[1:] {
|
|
||||||
switch method {
|
|
||||||
case Socks5AuthNotRequired:
|
|
||||||
s.authReqMethod = Socks5AuthNotRequired
|
|
||||||
return utils.LSMActionNext
|
|
||||||
case Socks5AuthPassword:
|
|
||||||
s.authReqMethod = Socks5AuthPassword
|
|
||||||
return utils.LSMActionNext
|
|
||||||
default:
|
|
||||||
// TODO: more auth method to support
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks5ReqAuth() utils.LSMAction {
|
|
||||||
switch s.authReqMethod {
|
|
||||||
case Socks5AuthNotRequired:
|
|
||||||
s.reqMap["auth"] = analyzer.PropMap{"method": s.authReqMethod}
|
|
||||||
case Socks5AuthPassword:
|
|
||||||
meta, ok := s.reqBuf.Get(2, false)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
if meta[0] != 0x01 {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
usernameLen := int(meta[1])
|
|
||||||
meta, ok = s.reqBuf.Get(usernameLen+3, false)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
passwordLen := int(meta[usernameLen+2])
|
|
||||||
meta, ok = s.reqBuf.Get(usernameLen+passwordLen+3, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
s.authUsername = string(meta[2 : usernameLen+2])
|
|
||||||
s.authPassword = string(meta[usernameLen+3:])
|
|
||||||
s.reqMap["auth"] = analyzer.PropMap{
|
|
||||||
"method": s.authReqMethod,
|
|
||||||
"username": s.authUsername,
|
|
||||||
"password": s.authPassword,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.reqUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks5ReqConnInfo() utils.LSMAction {
|
|
||||||
/* preInfo struct
|
|
||||||
+----+-----+-------+------+-------------+
|
|
||||||
|VER | CMD | RSV | ATYP | DST.ADDR(1) |
|
|
||||||
+----+-----+-------+------+-------------+
|
|
||||||
*/
|
|
||||||
preInfo, ok := s.reqBuf.Get(5, false)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify socks version
|
|
||||||
if preInfo[0] != Socks5Version {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
var pktLen int
|
|
||||||
switch int(preInfo[3]) {
|
|
||||||
case Socks5AddrTypeIPv4:
|
|
||||||
pktLen = 10
|
|
||||||
case Socks5AddrTypeDomain:
|
|
||||||
domainLen := int(preInfo[4])
|
|
||||||
pktLen = 7 + domainLen
|
|
||||||
case Socks5AddrTypeIPv6:
|
|
||||||
pktLen = 22
|
|
||||||
default:
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt, ok := s.reqBuf.Get(pktLen, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse cmd
|
|
||||||
cmd := int(pkt[1])
|
|
||||||
if cmd != Socks5CmdTCPConnect && cmd != Socks5CmdTCPBind && cmd != Socks5CmdUDPAssociate {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.reqMap["cmd"] = cmd
|
|
||||||
|
|
||||||
// parse addr type
|
|
||||||
addrType := int(pkt[3])
|
|
||||||
var addr string
|
|
||||||
switch addrType {
|
|
||||||
case Socks5AddrTypeIPv4:
|
|
||||||
addr = net.IPv4(pkt[4], pkt[5], pkt[6], pkt[7]).String()
|
|
||||||
case Socks5AddrTypeDomain:
|
|
||||||
addr = string(pkt[5 : 5+pkt[4]])
|
|
||||||
case Socks5AddrTypeIPv6:
|
|
||||||
addr = net.IP(pkt[4 : 4+net.IPv6len]).String()
|
|
||||||
default:
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.reqMap["addr_type"] = addrType
|
|
||||||
s.reqMap["addr"] = addr
|
|
||||||
|
|
||||||
// parse port
|
|
||||||
port := int(pkt[pktLen-2])<<8 | int(pkt[pktLen-1])
|
|
||||||
s.reqMap["port"] = port
|
|
||||||
s.reqUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks5RespMethod() utils.LSMAction {
|
|
||||||
method, ok := s.respBuf.Get(1, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
s.authRespMethod = int(method[0])
|
|
||||||
s.respMap = make(analyzer.PropMap)
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks5RespAuth() utils.LSMAction {
|
|
||||||
switch s.authRespMethod {
|
|
||||||
case Socks5AuthNotRequired:
|
|
||||||
s.respMap["auth"] = analyzer.PropMap{"method": s.authRespMethod}
|
|
||||||
case Socks5AuthPassword:
|
|
||||||
authResp, ok := s.respBuf.Get(2, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
if authResp[0] != 0x01 {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
authStatus := int(authResp[1])
|
|
||||||
s.respMap["auth"] = analyzer.PropMap{
|
|
||||||
"method": s.authRespMethod,
|
|
||||||
"status": authStatus,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.respUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks5RespConnInfo() utils.LSMAction {
|
|
||||||
/* preInfo struct
|
|
||||||
+----+-----+-------+------+-------------+
|
|
||||||
|VER | REP | RSV | ATYP | BND.ADDR(1) |
|
|
||||||
+----+-----+-------+------+-------------+
|
|
||||||
*/
|
|
||||||
preInfo, ok := s.respBuf.Get(5, false)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify socks version
|
|
||||||
if preInfo[0] != Socks5Version {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
var pktLen int
|
|
||||||
switch int(preInfo[3]) {
|
|
||||||
case Socks5AddrTypeIPv4:
|
|
||||||
pktLen = 10
|
|
||||||
case Socks5AddrTypeDomain:
|
|
||||||
domainLen := int(preInfo[4])
|
|
||||||
pktLen = 7 + domainLen
|
|
||||||
case Socks5AddrTypeIPv6:
|
|
||||||
pktLen = 22
|
|
||||||
default:
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt, ok := s.respBuf.Get(pktLen, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse rep
|
|
||||||
rep := int(pkt[1])
|
|
||||||
s.respMap["rep"] = rep
|
|
||||||
|
|
||||||
// parse addr type
|
|
||||||
addrType := int(pkt[3])
|
|
||||||
var addr string
|
|
||||||
switch addrType {
|
|
||||||
case Socks5AddrTypeIPv4:
|
|
||||||
addr = net.IPv4(pkt[4], pkt[5], pkt[6], pkt[7]).String()
|
|
||||||
case Socks5AddrTypeDomain:
|
|
||||||
addr = string(pkt[5 : 5+pkt[4]])
|
|
||||||
case Socks5AddrTypeIPv6:
|
|
||||||
addr = net.IP(pkt[4 : 4+net.IPv6len]).String()
|
|
||||||
default:
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
s.respMap["addr_type"] = addrType
|
|
||||||
s.respMap["addr"] = addr
|
|
||||||
|
|
||||||
// parse port
|
|
||||||
port := int(pkt[pktLen-2])<<8 | int(pkt[pktLen-1])
|
|
||||||
s.respMap["port"] = port
|
|
||||||
s.respUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks4ReqIpAndPort() utils.LSMAction {
|
|
||||||
/* Following field will be parsed in this state:
|
|
||||||
+-----+----------+--------+
|
|
||||||
| CMD | DST.PORT | DST.IP |
|
|
||||||
+-----+----------+--------+
|
|
||||||
*/
|
|
||||||
pkt, ok := s.reqBuf.Get(7, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
if pkt[0] != Socks4CmdTCPConnect && pkt[0] != Socks4CmdTCPBind {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
dstPort := uint16(pkt[1])<<8 | uint16(pkt[2])
|
|
||||||
dstIp := net.IPv4(pkt[3], pkt[4], pkt[5], pkt[6]).String()
|
|
||||||
|
|
||||||
// Socks4a extension
|
|
||||||
if pkt[3] == 0 && pkt[4] == 0 && pkt[5] == 0 {
|
|
||||||
s.version = Socks4A
|
|
||||||
}
|
|
||||||
|
|
||||||
s.reqMap["cmd"] = pkt[0]
|
|
||||||
s.reqMap["addr"] = dstIp
|
|
||||||
s.reqMap["addr_type"] = Socks5AddrTypeIPv4
|
|
||||||
s.reqMap["port"] = dstPort
|
|
||||||
s.reqUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks4ReqUserId() utils.LSMAction {
|
|
||||||
userIdSlice, ok := s.reqBuf.GetUntil([]byte("\x00"), true, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
userId := string(userIdSlice[:len(userIdSlice)-1])
|
|
||||||
s.reqMap["auth"] = analyzer.PropMap{
|
|
||||||
"user_id": userId,
|
|
||||||
}
|
|
||||||
s.reqUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks4ReqHostname() utils.LSMAction {
|
|
||||||
// Only Socks4a support hostname
|
|
||||||
if s.version != Socks4A {
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
hostnameSlice, ok := s.reqBuf.GetUntil([]byte("\x00"), true, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
hostname := string(hostnameSlice[:len(hostnameSlice)-1])
|
|
||||||
s.reqMap["addr"] = hostname
|
|
||||||
s.reqMap["addr_type"] = Socks5AddrTypeDomain
|
|
||||||
s.reqUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) parseSocks4RespPacket() utils.LSMAction {
|
|
||||||
pkt, ok := s.respBuf.Get(7, true)
|
|
||||||
if !ok {
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
if pkt[0] != Socks4ReqGranted &&
|
|
||||||
pkt[0] != Socks4ReqRejectOrFailed &&
|
|
||||||
pkt[0] != Socks4ReqRejectIdentd &&
|
|
||||||
pkt[0] != Socks4ReqRejectUser {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
dstPort := uint16(pkt[1])<<8 | uint16(pkt[2])
|
|
||||||
dstIp := net.IPv4(pkt[3], pkt[4], pkt[5], pkt[6]).String()
|
|
||||||
s.respMap = analyzer.PropMap{
|
|
||||||
"rep": pkt[0],
|
|
||||||
"addr": dstIp,
|
|
||||||
"addr_type": Socks5AddrTypeIPv4,
|
|
||||||
"port": dstPort,
|
|
||||||
}
|
|
||||||
s.respUpdated = true
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *socksStream) socksVersion() int {
|
|
||||||
switch s.version {
|
|
||||||
case Socks4, Socks4A:
|
|
||||||
return Socks4Version
|
|
||||||
case Socks5:
|
|
||||||
return Socks5Version
|
|
||||||
default:
|
|
||||||
return SocksInvalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
package tcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/analyzer/internal"
|
|
||||||
"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.tlsClientHelloPreprocess,
|
|
||||||
s.parseClientHelloData,
|
|
||||||
)
|
|
||||||
s.respLSM = utils.NewLinearStateMachine(
|
|
||||||
s.tlsServerHelloPreprocess,
|
|
||||||
s.parseServerHelloData,
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tlsClientHelloPreprocess validates ClientHello message.
|
|
||||||
//
|
|
||||||
// During validation, message header and first handshake header may be removed
|
|
||||||
// from `s.reqBuf`.
|
|
||||||
func (s *tlsStream) tlsClientHelloPreprocess() utils.LSMAction {
|
|
||||||
// headers size: content type (1 byte) + legacy protocol version (2 bytes) +
|
|
||||||
// + content length (2 bytes) + message type (1 byte) +
|
|
||||||
// + handshake length (3 bytes)
|
|
||||||
const headersSize = 9
|
|
||||||
|
|
||||||
// minimal data size: protocol version (2 bytes) + random (32 bytes) +
|
|
||||||
// + session ID (1 byte) + cipher suites (4 bytes) +
|
|
||||||
// + compression methods (2 bytes) + no extensions
|
|
||||||
const minDataSize = 41
|
|
||||||
|
|
||||||
header, ok := s.reqBuf.Get(headersSize, true)
|
|
||||||
if !ok {
|
|
||||||
// not a full header yet
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
if header[0] != internal.RecordTypeHandshake || header[5] != internal.TypeClientHello {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
s.clientHelloLen = int(header[6])<<16 | int(header[7])<<8 | int(header[8])
|
|
||||||
if s.clientHelloLen < minDataSize {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: something is missing. See:
|
|
||||||
// const messageHeaderSize = 4
|
|
||||||
// fullMessageLen := int(header[3])<<8 | int(header[4])
|
|
||||||
// msgNo := fullMessageLen / int(messageHeaderSize+s.serverHelloLen)
|
|
||||||
// if msgNo != 1 {
|
|
||||||
// // what here?
|
|
||||||
// }
|
|
||||||
// if messageNo != int(messageNo) {
|
|
||||||
// // what here?
|
|
||||||
// }
|
|
||||||
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
// tlsServerHelloPreprocess validates ServerHello message.
|
|
||||||
//
|
|
||||||
// During validation, message header and first handshake header may be removed
|
|
||||||
// from `s.reqBuf`.
|
|
||||||
func (s *tlsStream) tlsServerHelloPreprocess() utils.LSMAction {
|
|
||||||
// header size: content type (1 byte) + legacy protocol version (2 byte) +
|
|
||||||
// + content length (2 byte) + message type (1 byte) +
|
|
||||||
// + handshake length (3 byte)
|
|
||||||
const headersSize = 9
|
|
||||||
|
|
||||||
// minimal data size: server version (2 byte) + random (32 byte) +
|
|
||||||
// + session ID (>=1 byte) + cipher suite (2 byte) +
|
|
||||||
// + compression method (1 byte) + no extensions
|
|
||||||
const minDataSize = 38
|
|
||||||
|
|
||||||
header, ok := s.respBuf.Get(headersSize, true)
|
|
||||||
if !ok {
|
|
||||||
// not a full header yet
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
if header[0] != internal.RecordTypeHandshake || header[5] != internal.TypeServerHello {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
s.serverHelloLen = int(header[6])<<16 | int(header[7])<<8 | int(header[8])
|
|
||||||
if s.serverHelloLen < minDataSize {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: something is missing. See example:
|
|
||||||
// const messageHeaderSize = 4
|
|
||||||
// fullMessageLen := int(header[3])<<8 | int(header[4])
|
|
||||||
// msgNo := fullMessageLen / int(messageHeaderSize+s.serverHelloLen)
|
|
||||||
// if msgNo != 1 {
|
|
||||||
// // what here?
|
|
||||||
// }
|
|
||||||
// if messageNo != int(messageNo) {
|
|
||||||
// // what here?
|
|
||||||
// }
|
|
||||||
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseClientHelloData converts valid ClientHello message data (without
|
|
||||||
// headers) into `analyzer.PropMap`.
|
|
||||||
//
|
|
||||||
// Parsing error may leave `s.reqBuf` in an unusable state.
|
|
||||||
func (s *tlsStream) parseClientHelloData() utils.LSMAction {
|
|
||||||
chBuf, ok := s.reqBuf.GetSubBuffer(s.clientHelloLen, true)
|
|
||||||
if !ok {
|
|
||||||
// Not a full client hello yet
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
m := internal.ParseTLSClientHelloMsgData(chBuf)
|
|
||||||
if m == nil {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
} else {
|
|
||||||
s.reqUpdated = true
|
|
||||||
s.reqMap = m
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseServerHelloData converts valid ServerHello message data (without
|
|
||||||
// headers) into `analyzer.PropMap`.
|
|
||||||
//
|
|
||||||
// Parsing error may leave `s.respBuf` in an unusable state.
|
|
||||||
func (s *tlsStream) parseServerHelloData() utils.LSMAction {
|
|
||||||
shBuf, ok := s.respBuf.GetSubBuffer(s.serverHelloLen, true)
|
|
||||||
if !ok {
|
|
||||||
// Not a full server hello yet
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
m := internal.ParseTLSServerHelloMsgData(shBuf)
|
|
||||||
if m == nil {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
} else {
|
|
||||||
s.respUpdated = true
|
|
||||||
s.respMap = m
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tlsStream) Close(limited bool) *analyzer.PropUpdate {
|
|
||||||
s.reqBuf.Reset()
|
|
||||||
s.respBuf.Reset()
|
|
||||||
s.reqMap = nil
|
|
||||||
s.respMap = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,517 +0,0 @@
|
|||||||
package tcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ analyzer.TCPAnalyzer = (*TrojanAnalyzer)(nil)
|
|
||||||
|
|
||||||
// CCS stands for "Change Cipher Spec"
|
|
||||||
var ccsPattern = []byte{20, 3, 3, 0, 1, 1}
|
|
||||||
|
|
||||||
// TrojanAnalyzer uses length-based heuristics to detect Trojan traffic based on
|
|
||||||
// its "TLS-in-TLS" nature. The heuristics are trained using a decision tree with
|
|
||||||
// about 20k Trojan samples and 30k non-Trojan samples. The tree is then converted
|
|
||||||
// to code using a custom tool and inlined here (isTrojanSeq function).
|
|
||||||
// Accuracy: 1% false positive rate, 10% false negative rate.
|
|
||||||
// We do NOT recommend directly blocking all positive connections, as this may
|
|
||||||
// break legitimate TLS connections.
|
|
||||||
type TrojanAnalyzer struct{}
|
|
||||||
|
|
||||||
func (a *TrojanAnalyzer) Name() string {
|
|
||||||
return "trojan"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TrojanAnalyzer) Limit() int {
|
|
||||||
return 512000
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TrojanAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
|
|
||||||
return newTrojanStream(logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
type trojanStream struct {
|
|
||||||
logger analyzer.Logger
|
|
||||||
first bool
|
|
||||||
count bool
|
|
||||||
rev bool
|
|
||||||
seq [4]int
|
|
||||||
seqIndex int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTrojanStream(logger analyzer.Logger) *trojanStream {
|
|
||||||
return &trojanStream{logger: logger}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *trojanStream) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.first {
|
|
||||||
s.first = false
|
|
||||||
// Stop if it's not a valid TLS connection
|
|
||||||
if !(!rev && len(data) >= 3 && data[0] >= 0x16 && data[0] <= 0x17 &&
|
|
||||||
data[1] == 0x03 && data[2] <= 0x09) {
|
|
||||||
return nil, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !rev && !s.count && len(data) >= 6 && bytes.Equal(data[:6], ccsPattern) {
|
|
||||||
// Client Change Cipher Spec encountered, start counting
|
|
||||||
s.count = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.count {
|
|
||||||
if rev == s.rev {
|
|
||||||
// Same direction as last time, just update the number
|
|
||||||
s.seq[s.seqIndex] += len(data)
|
|
||||||
} else {
|
|
||||||
// Different direction, bump the index
|
|
||||||
s.seqIndex += 1
|
|
||||||
if s.seqIndex == 4 {
|
|
||||||
return &analyzer.PropUpdate{
|
|
||||||
Type: analyzer.PropUpdateReplace,
|
|
||||||
M: analyzer.PropMap{
|
|
||||||
"seq": s.seq,
|
|
||||||
"yes": isTrojanSeq(s.seq),
|
|
||||||
},
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
s.seq[s.seqIndex] += len(data)
|
|
||||||
s.rev = rev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *trojanStream) Close(limited bool) *analyzer.PropUpdate {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTrojanSeq(seq [4]int) bool {
|
|
||||||
length1 := seq[0]
|
|
||||||
length2 := seq[1]
|
|
||||||
length3 := seq[2]
|
|
||||||
length4 := seq[3]
|
|
||||||
|
|
||||||
if length2 <= 2431 {
|
|
||||||
if length2 <= 157 {
|
|
||||||
if length1 <= 156 {
|
|
||||||
if length3 <= 108 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 892 {
|
|
||||||
if length3 <= 40 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length3 <= 788 {
|
|
||||||
if length4 <= 185 {
|
|
||||||
if length1 <= 411 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 112 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length3 <= 1346 {
|
|
||||||
if length1 <= 418 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 120 {
|
|
||||||
if length2 <= 63 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length4 <= 653 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 206 {
|
|
||||||
if length1 <= 185 {
|
|
||||||
if length1 <= 171 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length4 <= 211 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 251 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 286 {
|
|
||||||
if length1 <= 1123 {
|
|
||||||
if length3 <= 70 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length1 <= 659 {
|
|
||||||
if length3 <= 370 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 272 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 537 {
|
|
||||||
if length2 <= 276 {
|
|
||||||
if length3 <= 1877 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 1466 {
|
|
||||||
if length1 <= 1435 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 193 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 284 {
|
|
||||||
if length1 <= 277 {
|
|
||||||
if length2 <= 726 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length2 <= 768 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 782 {
|
|
||||||
if length4 <= 783 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 492 {
|
|
||||||
if length2 <= 396 {
|
|
||||||
if length2 <= 322 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 971 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 2128 {
|
|
||||||
if length2 <= 1418 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length3 <= 103 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 6232 {
|
|
||||||
if length3 <= 85 {
|
|
||||||
if length2 <= 3599 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length1 <= 613 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length3 <= 220 {
|
|
||||||
if length4 <= 1173 {
|
|
||||||
if length1 <= 874 {
|
|
||||||
if length4 <= 337 {
|
|
||||||
if length4 <= 68 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 667 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length3 <= 108 {
|
|
||||||
if length1 <= 1930 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 5383 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 664 {
|
|
||||||
if length3 <= 411 {
|
|
||||||
if length3 <= 383 {
|
|
||||||
if length4 <= 346 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 445 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 3708 {
|
|
||||||
if length4 <= 307 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 4656 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 1055 {
|
|
||||||
if length3 <= 580 {
|
|
||||||
if length1 <= 724 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 678 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 5352 {
|
|
||||||
if length3 <= 1586 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 2173 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 9408 {
|
|
||||||
if length1 <= 670 {
|
|
||||||
if length4 <= 76 {
|
|
||||||
if length3 <= 175 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 9072 {
|
|
||||||
if length3 <= 314 {
|
|
||||||
if length3 <= 179 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 708 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 795 {
|
|
||||||
if length2 <= 6334 {
|
|
||||||
if length2 <= 6288 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 6404 {
|
|
||||||
if length2 <= 8194 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 8924 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length3 <= 732 {
|
|
||||||
if length1 <= 1397 {
|
|
||||||
if length3 <= 179 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 1976 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length1 <= 2840 {
|
|
||||||
if length1 <= 2591 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 30 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length2 <= 13314 {
|
|
||||||
if length4 <= 1786 {
|
|
||||||
if length2 <= 13018 {
|
|
||||||
if length4 <= 869 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length3 <= 775 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length4 <= 73 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if length3 <= 640 {
|
|
||||||
if length3 <= 237 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if length2 <= 43804 {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
package udp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dnsUDPInvalidCountThreshold = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
invalidCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *dnsUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
|
|
||||||
m := parseDNSMessage(data)
|
|
||||||
// To allow non-DNS UDP traffic to get offloaded,
|
|
||||||
// we consider a UDP stream invalid and "done" if
|
|
||||||
// it has more than a certain number of consecutive
|
|
||||||
// packets that are not valid DNS messages.
|
|
||||||
if m == nil {
|
|
||||||
s.invalidCount++
|
|
||||||
return nil, s.invalidCount >= dnsUDPInvalidCountThreshold
|
|
||||||
}
|
|
||||||
s.invalidCount = 0 // Reset invalid count on valid DNS message
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,384 +0,0 @@
|
|||||||
package udp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ analyzer.UDPAnalyzer = (*OpenVPNAnalyzer)(nil)
|
|
||||||
_ analyzer.TCPAnalyzer = (*OpenVPNAnalyzer)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ analyzer.UDPStream = (*openvpnUDPStream)(nil)
|
|
||||||
_ analyzer.TCPStream = (*openvpnTCPStream)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ref paper:
|
|
||||||
// https://www.usenix.org/system/files/sec22fall_xue-diwen.pdf
|
|
||||||
|
|
||||||
// OpenVPN Opcodes definitions from:
|
|
||||||
// https://github.com/OpenVPN/openvpn/blob/master/src/openvpn/ssl_pkt.h
|
|
||||||
const (
|
|
||||||
OpenVPNControlHardResetClientV1 = 1
|
|
||||||
OpenVPNControlHardResetServerV1 = 2
|
|
||||||
OpenVPNControlSoftResetV1 = 3
|
|
||||||
OpenVPNControlV1 = 4
|
|
||||||
OpenVPNAckV1 = 5
|
|
||||||
OpenVPNDataV1 = 6
|
|
||||||
OpenVPNControlHardResetClientV2 = 7
|
|
||||||
OpenVPNControlHardResetServerV2 = 8
|
|
||||||
OpenVPNDataV2 = 9
|
|
||||||
OpenVPNControlHardResetClientV3 = 10
|
|
||||||
OpenVPNControlWkcV1 = 11
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
OpenVPNMinPktLen = 6
|
|
||||||
OpenVPNTCPPktDefaultLimit = 256
|
|
||||||
OpenVPNUDPPktDefaultLimit = 256
|
|
||||||
)
|
|
||||||
|
|
||||||
type OpenVPNAnalyzer struct{}
|
|
||||||
|
|
||||||
func (a *OpenVPNAnalyzer) Name() string {
|
|
||||||
return "openvpn"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *OpenVPNAnalyzer) Limit() int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *OpenVPNAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
|
||||||
return newOpenVPNUDPStream(logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *OpenVPNAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
|
|
||||||
return newOpenVPNTCPStream(logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
type openvpnPkt struct {
|
|
||||||
pktLen uint16 // 16 bits, TCP proto only
|
|
||||||
opcode byte // 5 bits
|
|
||||||
_keyId byte // 3 bits, not used
|
|
||||||
|
|
||||||
// We don't care about the rest of the packet
|
|
||||||
// payload []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type openvpnStream struct {
|
|
||||||
logger analyzer.Logger
|
|
||||||
|
|
||||||
reqUpdated bool
|
|
||||||
reqLSM *utils.LinearStateMachine
|
|
||||||
reqDone bool
|
|
||||||
|
|
||||||
respUpdated bool
|
|
||||||
respLSM *utils.LinearStateMachine
|
|
||||||
respDone bool
|
|
||||||
|
|
||||||
rxPktCnt int
|
|
||||||
txPktCnt int
|
|
||||||
pktLimit int
|
|
||||||
|
|
||||||
reqPktParse func() (*openvpnPkt, utils.LSMAction)
|
|
||||||
respPktParse func() (*openvpnPkt, utils.LSMAction)
|
|
||||||
|
|
||||||
lastOpcode byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnStream) parseCtlHardResetClient() utils.LSMAction {
|
|
||||||
pkt, action := o.reqPktParse()
|
|
||||||
if action != utils.LSMActionNext {
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
|
|
||||||
if pkt.opcode != OpenVPNControlHardResetClientV1 &&
|
|
||||||
pkt.opcode != OpenVPNControlHardResetClientV2 &&
|
|
||||||
pkt.opcode != OpenVPNControlHardResetClientV3 {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
o.lastOpcode = pkt.opcode
|
|
||||||
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnStream) parseCtlHardResetServer() utils.LSMAction {
|
|
||||||
if o.lastOpcode != OpenVPNControlHardResetClientV1 &&
|
|
||||||
o.lastOpcode != OpenVPNControlHardResetClientV2 &&
|
|
||||||
o.lastOpcode != OpenVPNControlHardResetClientV3 {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt, action := o.respPktParse()
|
|
||||||
if action != utils.LSMActionNext {
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
|
|
||||||
if pkt.opcode != OpenVPNControlHardResetServerV1 &&
|
|
||||||
pkt.opcode != OpenVPNControlHardResetServerV2 {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
o.lastOpcode = pkt.opcode
|
|
||||||
|
|
||||||
return utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnStream) parseReq() utils.LSMAction {
|
|
||||||
pkt, action := o.reqPktParse()
|
|
||||||
if action != utils.LSMActionNext {
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
|
|
||||||
if pkt.opcode != OpenVPNControlSoftResetV1 &&
|
|
||||||
pkt.opcode != OpenVPNControlV1 &&
|
|
||||||
pkt.opcode != OpenVPNAckV1 &&
|
|
||||||
pkt.opcode != OpenVPNDataV1 &&
|
|
||||||
pkt.opcode != OpenVPNDataV2 &&
|
|
||||||
pkt.opcode != OpenVPNControlWkcV1 {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
o.txPktCnt += 1
|
|
||||||
o.reqUpdated = true
|
|
||||||
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnStream) parseResp() utils.LSMAction {
|
|
||||||
pkt, action := o.respPktParse()
|
|
||||||
if action != utils.LSMActionNext {
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
|
|
||||||
if pkt.opcode != OpenVPNControlSoftResetV1 &&
|
|
||||||
pkt.opcode != OpenVPNControlV1 &&
|
|
||||||
pkt.opcode != OpenVPNAckV1 &&
|
|
||||||
pkt.opcode != OpenVPNDataV1 &&
|
|
||||||
pkt.opcode != OpenVPNDataV2 &&
|
|
||||||
pkt.opcode != OpenVPNControlWkcV1 {
|
|
||||||
return utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
o.rxPktCnt += 1
|
|
||||||
o.respUpdated = true
|
|
||||||
|
|
||||||
return utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
type openvpnUDPStream struct {
|
|
||||||
openvpnStream
|
|
||||||
curPkt []byte
|
|
||||||
// We don't introduce `invalidCount` here to decrease the false positive rate
|
|
||||||
// invalidCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOpenVPNUDPStream(logger analyzer.Logger) *openvpnUDPStream {
|
|
||||||
s := &openvpnUDPStream{
|
|
||||||
openvpnStream: openvpnStream{
|
|
||||||
logger: logger,
|
|
||||||
pktLimit: OpenVPNUDPPktDefaultLimit,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
s.respPktParse = s.parsePkt
|
|
||||||
s.reqPktParse = s.parsePkt
|
|
||||||
s.reqLSM = utils.NewLinearStateMachine(
|
|
||||||
s.parseCtlHardResetClient,
|
|
||||||
s.parseReq,
|
|
||||||
)
|
|
||||||
s.respLSM = utils.NewLinearStateMachine(
|
|
||||||
s.parseCtlHardResetServer,
|
|
||||||
s.parseResp,
|
|
||||||
)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, d bool) {
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
var update *analyzer.PropUpdate
|
|
||||||
var cancelled bool
|
|
||||||
o.curPkt = data
|
|
||||||
if rev {
|
|
||||||
o.respUpdated = false
|
|
||||||
cancelled, o.respDone = o.respLSM.Run()
|
|
||||||
if o.respUpdated {
|
|
||||||
update = &analyzer.PropUpdate{
|
|
||||||
Type: analyzer.PropUpdateReplace,
|
|
||||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
|
||||||
}
|
|
||||||
o.respUpdated = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
o.reqUpdated = false
|
|
||||||
cancelled, o.reqDone = o.reqLSM.Run()
|
|
||||||
if o.reqUpdated {
|
|
||||||
update = &analyzer.PropUpdate{
|
|
||||||
Type: analyzer.PropUpdateReplace,
|
|
||||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
|
||||||
}
|
|
||||||
o.reqUpdated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnUDPStream) Close(limited bool) *analyzer.PropUpdate {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse OpenVPN UDP packet.
|
|
||||||
func (o *openvpnUDPStream) parsePkt() (p *openvpnPkt, action utils.LSMAction) {
|
|
||||||
if o.curPkt == nil {
|
|
||||||
return nil, utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
if !OpenVPNCheckForValidOpcode(o.curPkt[0] >> 3) {
|
|
||||||
return nil, utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse packet header
|
|
||||||
p = &openvpnPkt{}
|
|
||||||
p.opcode = o.curPkt[0] >> 3
|
|
||||||
p._keyId = o.curPkt[0] & 0x07
|
|
||||||
|
|
||||||
o.curPkt = nil
|
|
||||||
return p, utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
type openvpnTCPStream struct {
|
|
||||||
openvpnStream
|
|
||||||
reqBuf *utils.ByteBuffer
|
|
||||||
respBuf *utils.ByteBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOpenVPNTCPStream(logger analyzer.Logger) *openvpnTCPStream {
|
|
||||||
s := &openvpnTCPStream{
|
|
||||||
openvpnStream: openvpnStream{
|
|
||||||
logger: logger,
|
|
||||||
pktLimit: OpenVPNTCPPktDefaultLimit,
|
|
||||||
},
|
|
||||||
reqBuf: &utils.ByteBuffer{},
|
|
||||||
respBuf: &utils.ByteBuffer{},
|
|
||||||
}
|
|
||||||
s.respPktParse = func() (*openvpnPkt, utils.LSMAction) {
|
|
||||||
return s.parsePkt(true)
|
|
||||||
}
|
|
||||||
s.reqPktParse = func() (*openvpnPkt, utils.LSMAction) {
|
|
||||||
return s.parsePkt(false)
|
|
||||||
}
|
|
||||||
s.reqLSM = utils.NewLinearStateMachine(
|
|
||||||
s.parseCtlHardResetClient,
|
|
||||||
s.parseReq,
|
|
||||||
)
|
|
||||||
s.respLSM = utils.NewLinearStateMachine(
|
|
||||||
s.parseCtlHardResetServer,
|
|
||||||
s.parseResp,
|
|
||||||
)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnTCPStream) 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 {
|
|
||||||
o.respBuf.Append(data)
|
|
||||||
o.respUpdated = false
|
|
||||||
cancelled, o.respDone = o.respLSM.Run()
|
|
||||||
if o.respUpdated {
|
|
||||||
update = &analyzer.PropUpdate{
|
|
||||||
Type: analyzer.PropUpdateReplace,
|
|
||||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
|
||||||
}
|
|
||||||
o.respUpdated = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
o.reqBuf.Append(data)
|
|
||||||
o.reqUpdated = false
|
|
||||||
cancelled, o.reqDone = o.reqLSM.Run()
|
|
||||||
if o.reqUpdated {
|
|
||||||
update = &analyzer.PropUpdate{
|
|
||||||
Type: analyzer.PropUpdateMerge,
|
|
||||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
|
||||||
}
|
|
||||||
o.reqUpdated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *openvpnTCPStream) Close(limited bool) *analyzer.PropUpdate {
|
|
||||||
o.reqBuf.Reset()
|
|
||||||
o.respBuf.Reset()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse OpenVPN TCP packet.
|
|
||||||
func (o *openvpnTCPStream) parsePkt(rev bool) (p *openvpnPkt, action utils.LSMAction) {
|
|
||||||
var buffer *utils.ByteBuffer
|
|
||||||
if rev {
|
|
||||||
buffer = o.respBuf
|
|
||||||
} else {
|
|
||||||
buffer = o.reqBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse packet length
|
|
||||||
pktLen, ok := buffer.GetUint16(false, false)
|
|
||||||
if !ok {
|
|
||||||
return nil, utils.LSMActionPause
|
|
||||||
}
|
|
||||||
|
|
||||||
if pktLen < OpenVPNMinPktLen {
|
|
||||||
return nil, utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
pktOp, ok := buffer.Get(3, false)
|
|
||||||
if !ok {
|
|
||||||
return nil, utils.LSMActionPause
|
|
||||||
}
|
|
||||||
if !OpenVPNCheckForValidOpcode(pktOp[2] >> 3) {
|
|
||||||
return nil, utils.LSMActionCancel
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt, ok := buffer.Get(int(pktLen)+2, true)
|
|
||||||
if !ok {
|
|
||||||
return nil, utils.LSMActionPause
|
|
||||||
}
|
|
||||||
pkt = pkt[2:]
|
|
||||||
|
|
||||||
// Parse packet header
|
|
||||||
p = &openvpnPkt{}
|
|
||||||
p.pktLen = pktLen
|
|
||||||
p.opcode = pkt[0] >> 3
|
|
||||||
p._keyId = pkt[0] & 0x07
|
|
||||||
|
|
||||||
return p, utils.LSMActionNext
|
|
||||||
}
|
|
||||||
|
|
||||||
func OpenVPNCheckForValidOpcode(opcode byte) bool {
|
|
||||||
switch opcode {
|
|
||||||
case OpenVPNControlHardResetClientV1,
|
|
||||||
OpenVPNControlHardResetServerV1,
|
|
||||||
OpenVPNControlSoftResetV1,
|
|
||||||
OpenVPNControlV1,
|
|
||||||
OpenVPNAckV1,
|
|
||||||
OpenVPNDataV1,
|
|
||||||
OpenVPNControlHardResetClientV2,
|
|
||||||
OpenVPNControlHardResetServerV2,
|
|
||||||
OpenVPNDataV2,
|
|
||||||
OpenVPNControlHardResetClientV3,
|
|
||||||
OpenVPNControlWkcV1:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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) {
|
|
||||||
// minimal data size: protocol version (2 bytes) + random (32 bytes) +
|
|
||||||
// + session ID (1 byte) + cipher suites (4 bytes) +
|
|
||||||
// + compression methods (2 bytes) + no extensions
|
|
||||||
const minDataSize = 41
|
|
||||||
|
|
||||||
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 { // FIXME: isn't length checked inside quic.ReadCryptoPayload? Also, what about error handling?
|
|
||||||
s.invalidCount++
|
|
||||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
|
||||||
}
|
|
||||||
|
|
||||||
if pl[0] != internal.TypeClientHello {
|
|
||||||
s.invalidCount++
|
|
||||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
|
||||||
}
|
|
||||||
|
|
||||||
chLen := int(pl[1])<<16 | int(pl[2])<<8 | int(pl[3])
|
|
||||||
if chLen < minDataSize {
|
|
||||||
s.invalidCount++
|
|
||||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
|
||||||
}
|
|
||||||
|
|
||||||
m := internal.ParseTLSClientHelloMsgData(&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
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
package udp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"container/ring"
|
|
||||||
"encoding/binary"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ analyzer.UDPAnalyzer = (*WireGuardAnalyzer)(nil)
|
|
||||||
_ analyzer.UDPStream = (*wireGuardUDPStream)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
wireguardUDPInvalidCountThreshold = 4
|
|
||||||
wireguardRememberedIndexCount = 6
|
|
||||||
wireguardPropKeyMessageType = "message_type"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
wireguardTypeHandshakeInitiation = 1
|
|
||||||
wireguardTypeHandshakeResponse = 2
|
|
||||||
wireguardTypeData = 4
|
|
||||||
wireguardTypeCookieReply = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
wireguardSizeHandshakeInitiation = 148
|
|
||||||
wireguardSizeHandshakeResponse = 92
|
|
||||||
wireguardMinSizePacketData = 32 // 16 bytes header + 16 bytes AEAD overhead
|
|
||||||
wireguardSizePacketCookieReply = 64
|
|
||||||
)
|
|
||||||
|
|
||||||
type WireGuardAnalyzer struct{}
|
|
||||||
|
|
||||||
func (a *WireGuardAnalyzer) Name() string {
|
|
||||||
return "wireguard"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *WireGuardAnalyzer) Limit() int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *WireGuardAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
|
||||||
return newWireGuardUDPStream(logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
type wireGuardUDPStream struct {
|
|
||||||
logger analyzer.Logger
|
|
||||||
invalidCount int
|
|
||||||
rememberedIndexes *ring.Ring
|
|
||||||
rememberedIndexesLock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWireGuardUDPStream(logger analyzer.Logger) *wireGuardUDPStream {
|
|
||||||
return &wireGuardUDPStream{
|
|
||||||
logger: logger,
|
|
||||||
rememberedIndexes: ring.New(wireguardRememberedIndexCount),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
|
|
||||||
m := s.parseWireGuardPacket(rev, data)
|
|
||||||
if m == nil {
|
|
||||||
s.invalidCount++
|
|
||||||
return nil, s.invalidCount >= wireguardUDPInvalidCountThreshold
|
|
||||||
}
|
|
||||||
s.invalidCount = 0 // Reset invalid count on valid WireGuard packet
|
|
||||||
messageType := m[wireguardPropKeyMessageType].(byte)
|
|
||||||
propUpdateType := analyzer.PropUpdateMerge
|
|
||||||
if messageType == wireguardTypeHandshakeInitiation {
|
|
||||||
propUpdateType = analyzer.PropUpdateReplace
|
|
||||||
}
|
|
||||||
return &analyzer.PropUpdate{
|
|
||||||
Type: propUpdateType,
|
|
||||||
M: m,
|
|
||||||
}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) Close(limited bool) *analyzer.PropUpdate {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) parseWireGuardPacket(rev bool, data []byte) analyzer.PropMap {
|
|
||||||
if len(data) < 4 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if slices.Max(data[1:4]) != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
messageType := data[0]
|
|
||||||
var propKey string
|
|
||||||
var propValue analyzer.PropMap
|
|
||||||
switch messageType {
|
|
||||||
case wireguardTypeHandshakeInitiation:
|
|
||||||
propKey = "handshake_initiation"
|
|
||||||
propValue = s.parseWireGuardHandshakeInitiation(rev, data)
|
|
||||||
case wireguardTypeHandshakeResponse:
|
|
||||||
propKey = "handshake_response"
|
|
||||||
propValue = s.parseWireGuardHandshakeResponse(rev, data)
|
|
||||||
case wireguardTypeData:
|
|
||||||
propKey = "packet_data"
|
|
||||||
propValue = s.parseWireGuardPacketData(rev, data)
|
|
||||||
case wireguardTypeCookieReply:
|
|
||||||
propKey = "packet_cookie_reply"
|
|
||||||
propValue = s.parseWireGuardPacketCookieReply(rev, data)
|
|
||||||
}
|
|
||||||
if propValue == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m := make(analyzer.PropMap)
|
|
||||||
m[wireguardPropKeyMessageType] = messageType
|
|
||||||
m[propKey] = propValue
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) parseWireGuardHandshakeInitiation(rev bool, data []byte) analyzer.PropMap {
|
|
||||||
if len(data) != wireguardSizeHandshakeInitiation {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m := make(analyzer.PropMap)
|
|
||||||
|
|
||||||
senderIndex := binary.LittleEndian.Uint32(data[4:8])
|
|
||||||
m["sender_index"] = senderIndex
|
|
||||||
s.putSenderIndex(rev, senderIndex)
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) parseWireGuardHandshakeResponse(rev bool, data []byte) analyzer.PropMap {
|
|
||||||
if len(data) != wireguardSizeHandshakeResponse {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m := make(analyzer.PropMap)
|
|
||||||
|
|
||||||
senderIndex := binary.LittleEndian.Uint32(data[4:8])
|
|
||||||
m["sender_index"] = senderIndex
|
|
||||||
s.putSenderIndex(rev, senderIndex)
|
|
||||||
|
|
||||||
receiverIndex := binary.LittleEndian.Uint32(data[8:12])
|
|
||||||
m["receiver_index"] = receiverIndex
|
|
||||||
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) parseWireGuardPacketData(rev bool, data []byte) analyzer.PropMap {
|
|
||||||
if len(data) < wireguardMinSizePacketData {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(data)%16 != 0 {
|
|
||||||
// WireGuard zero padding the packet to make the length a multiple of 16
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m := make(analyzer.PropMap)
|
|
||||||
|
|
||||||
receiverIndex := binary.LittleEndian.Uint32(data[4:8])
|
|
||||||
m["receiver_index"] = receiverIndex
|
|
||||||
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
|
|
||||||
|
|
||||||
m["counter"] = binary.LittleEndian.Uint64(data[8:16])
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) parseWireGuardPacketCookieReply(rev bool, data []byte) analyzer.PropMap {
|
|
||||||
if len(data) != wireguardSizePacketCookieReply {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m := make(analyzer.PropMap)
|
|
||||||
|
|
||||||
receiverIndex := binary.LittleEndian.Uint32(data[4:8])
|
|
||||||
m["receiver_index"] = receiverIndex
|
|
||||||
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
type wireGuardIndex struct {
|
|
||||||
SenderIndex uint32
|
|
||||||
Reverse bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) putSenderIndex(rev bool, senderIndex uint32) {
|
|
||||||
s.rememberedIndexesLock.Lock()
|
|
||||||
defer s.rememberedIndexesLock.Unlock()
|
|
||||||
|
|
||||||
s.rememberedIndexes.Value = &wireGuardIndex{
|
|
||||||
SenderIndex: senderIndex,
|
|
||||||
Reverse: rev,
|
|
||||||
}
|
|
||||||
s.rememberedIndexes = s.rememberedIndexes.Prev()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *wireGuardUDPStream) matchReceiverIndex(rev bool, receiverIndex uint32) bool {
|
|
||||||
s.rememberedIndexesLock.RLock()
|
|
||||||
defer s.rememberedIndexesLock.RUnlock()
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
ris := s.rememberedIndexes
|
|
||||||
for it := ris.Next(); it != ris; it = it.Next() {
|
|
||||||
if it.Value == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
wgidx := it.Value.(*wireGuardIndex)
|
|
||||||
if wgidx.Reverse == !rev && wgidx.SenderIndex == receiverIndex {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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) AppendSteps(steps ...func() LSMAction) {
|
|
||||||
lsm.Steps = append(lsm.Steps, steps...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lsm *LinearStateMachine) Reset() {
|
|
||||||
lsm.index = 0
|
|
||||||
lsm.cancelled = false
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
func ByteSlicesToStrings(bss [][]byte) []string {
|
|
||||||
ss := make([]string, len(bss))
|
|
||||||
for i, bs := range bss {
|
|
||||||
ss[i] = string(bs)
|
|
||||||
}
|
|
||||||
return ss
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type configError struct {
|
|
||||||
Field string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e configError) Error() string {
|
|
||||||
return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e configError) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
@@ -1,544 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/analyzer/tcp"
|
|
||||||
"github.com/apernet/OpenGFW/analyzer/udp"
|
|
||||||
"github.com/apernet/OpenGFW/engine"
|
|
||||||
"github.com/apernet/OpenGFW/io"
|
|
||||||
"github.com/apernet/OpenGFW/modifier"
|
|
||||||
modUDP "github.com/apernet/OpenGFW/modifier/udp"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo"
|
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
appLogo = `
|
|
||||||
░█▀█░█▀█░█▀▀░█▀█░█▀▀░█▀▀░█░█
|
|
||||||
░█░█░█▀▀░█▀▀░█░█░█░█░█▀▀░█▄█
|
|
||||||
░▀▀▀░▀░░░▀▀▀░▀░▀░▀▀▀░▀░░░▀░▀
|
|
||||||
`
|
|
||||||
appDesc = "Open source network filtering and analysis software"
|
|
||||||
appAuthors = "Aperture Internet Laboratory <https://github.com/apernet>"
|
|
||||||
|
|
||||||
appLogLevelEnv = "OPENGFW_LOG_LEVEL"
|
|
||||||
appLogFormatEnv = "OPENGFW_LOG_FORMAT"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger *zap.Logger
|
|
||||||
|
|
||||||
// Flags
|
|
||||||
var (
|
|
||||||
cfgFile string
|
|
||||||
pcapFile string
|
|
||||||
logLevel string
|
|
||||||
logFormat string
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "OpenGFW [flags] rule_file",
|
|
||||||
Short: appDesc,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runMain,
|
|
||||||
}
|
|
||||||
|
|
||||||
var logLevelMap = map[string]zapcore.Level{
|
|
||||||
"debug": zapcore.DebugLevel,
|
|
||||||
"info": zapcore.InfoLevel,
|
|
||||||
"warn": zapcore.WarnLevel,
|
|
||||||
"error": zapcore.ErrorLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
var logFormatMap = map[string]zapcore.EncoderConfig{
|
|
||||||
"console": {
|
|
||||||
TimeKey: "time",
|
|
||||||
LevelKey: "level",
|
|
||||||
NameKey: "logger",
|
|
||||||
MessageKey: "msg",
|
|
||||||
LineEnding: zapcore.DefaultLineEnding,
|
|
||||||
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
|
||||||
EncodeTime: zapcore.RFC3339TimeEncoder,
|
|
||||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
|
||||||
},
|
|
||||||
"json": {
|
|
||||||
TimeKey: "time",
|
|
||||||
LevelKey: "level",
|
|
||||||
NameKey: "logger",
|
|
||||||
MessageKey: "msg",
|
|
||||||
LineEnding: zapcore.DefaultLineEnding,
|
|
||||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
|
||||||
EncodeTime: zapcore.EpochMillisTimeEncoder,
|
|
||||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analyzers & modifiers
|
|
||||||
|
|
||||||
var analyzers = []analyzer.Analyzer{
|
|
||||||
&tcp.FETAnalyzer{},
|
|
||||||
&tcp.HTTPAnalyzer{},
|
|
||||||
&tcp.SocksAnalyzer{},
|
|
||||||
&tcp.SSHAnalyzer{},
|
|
||||||
&tcp.TLSAnalyzer{},
|
|
||||||
&tcp.TrojanAnalyzer{},
|
|
||||||
&udp.DNSAnalyzer{},
|
|
||||||
&udp.OpenVPNAnalyzer{},
|
|
||||||
&udp.QUICAnalyzer{},
|
|
||||||
&udp.WireGuardAnalyzer{},
|
|
||||||
}
|
|
||||||
|
|
||||||
var modifiers = []modifier.Modifier{
|
|
||||||
&modUDP.DNSModifier{},
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute(args []string) {
|
|
||||||
rootCmd.SetArgs(args)
|
|
||||||
err := rootCmd.Execute()
|
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initFlags()
|
|
||||||
cobra.OnInitialize(initConfig)
|
|
||||||
cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFlags() {
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&pcapFile, "pcap", "p", "", "pcap file (optional)")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", envOrDefaultString(appLogLevelEnv, "info"), "log level")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "f", envOrDefaultString(appLogFormatEnv, "console"), "log format")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfig() {
|
|
||||||
if cfgFile != "" {
|
|
||||||
viper.SetConfigFile(cfgFile)
|
|
||||||
} else {
|
|
||||||
viper.SetConfigName("config")
|
|
||||||
viper.SetConfigType("yaml")
|
|
||||||
viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...)
|
|
||||||
viper.AddConfigPath(".")
|
|
||||||
viper.AddConfigPath("$HOME/.opengfw")
|
|
||||||
viper.AddConfigPath("/etc/opengfw")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLogger() {
|
|
||||||
level, ok := logLevelMap[strings.ToLower(logLevel)]
|
|
||||||
if !ok {
|
|
||||||
fmt.Printf("unsupported log level: %s\n", logLevel)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
enc, ok := logFormatMap[strings.ToLower(logFormat)]
|
|
||||||
if !ok {
|
|
||||||
fmt.Printf("unsupported log format: %s\n", logFormat)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
c := zap.Config{
|
|
||||||
Level: zap.NewAtomicLevelAt(level),
|
|
||||||
DisableCaller: true,
|
|
||||||
DisableStacktrace: true,
|
|
||||||
Encoding: strings.ToLower(logFormat),
|
|
||||||
EncoderConfig: enc,
|
|
||||||
OutputPaths: []string{"stderr"},
|
|
||||||
ErrorOutputPaths: []string{"stderr"},
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
logger, err = c.Build()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to initialize logger: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliConfig struct {
|
|
||||||
IO cliConfigIO `mapstructure:"io"`
|
|
||||||
Workers cliConfigWorkers `mapstructure:"workers"`
|
|
||||||
Ruleset cliConfigRuleset `mapstructure:"ruleset"`
|
|
||||||
Replay cliConfigReplay `mapstructure:"replay"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliConfigIO struct {
|
|
||||||
QueueSize uint32 `mapstructure:"queueSize"`
|
|
||||||
QueueNum *uint16 `mapstructure:"queueNum"`
|
|
||||||
Table string `mapstructure:"table"`
|
|
||||||
ConnMarkAccept uint32 `mapstructure:"connMarkAccept"`
|
|
||||||
ConnMarkDrop uint32 `mapstructure:"connMarkDrop"`
|
|
||||||
|
|
||||||
ReadBuffer int `mapstructure:"rcvBuf"`
|
|
||||||
WriteBuffer int `mapstructure:"sndBuf"`
|
|
||||||
Local bool `mapstructure:"local"`
|
|
||||||
RST bool `mapstructure:"rst"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliConfigReplay struct {
|
|
||||||
Realtime bool `mapstructure:"realtime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliConfigWorkers struct {
|
|
||||||
Count int `mapstructure:"count"`
|
|
||||||
QueueSize int `mapstructure:"queueSize"`
|
|
||||||
TCPMaxBufferedPagesTotal int `mapstructure:"tcpMaxBufferedPagesTotal"`
|
|
||||||
TCPMaxBufferedPagesPerConn int `mapstructure:"tcpMaxBufferedPagesPerConn"`
|
|
||||||
TCPTimeout time.Duration `mapstructure:"tcpTimeout"`
|
|
||||||
UDPMaxStreams int `mapstructure:"udpMaxStreams"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cliConfigRuleset struct {
|
|
||||||
GeoIp string `mapstructure:"geoip"`
|
|
||||||
GeoSite string `mapstructure:"geosite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliConfig) fillLogger(config *engine.Config) error {
|
|
||||||
config.Logger = &engineLogger{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliConfig) fillIO(config *engine.Config) error {
|
|
||||||
var ioImpl io.PacketIO
|
|
||||||
var err error
|
|
||||||
if pcapFile != "" {
|
|
||||||
// Setup IO for pcap file replay
|
|
||||||
logger.Info("replaying from pcap file", zap.String("pcap file", pcapFile))
|
|
||||||
ioImpl, err = io.NewPcapPacketIO(io.PcapPacketIOConfig{
|
|
||||||
PcapFile: pcapFile,
|
|
||||||
Realtime: c.Replay.Realtime,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Setup IO for nfqueue
|
|
||||||
ioImpl, err = io.NewNFQueuePacketIO(io.NFQueuePacketIOConfig{
|
|
||||||
QueueSize: c.IO.QueueSize,
|
|
||||||
QueueNum: c.IO.QueueNum,
|
|
||||||
Table: c.IO.Table,
|
|
||||||
ConnMarkAccept: c.IO.ConnMarkAccept,
|
|
||||||
ConnMarkDrop: c.IO.ConnMarkDrop,
|
|
||||||
|
|
||||||
ReadBuffer: c.IO.ReadBuffer,
|
|
||||||
WriteBuffer: c.IO.WriteBuffer,
|
|
||||||
Local: c.IO.Local,
|
|
||||||
RST: c.IO.RST,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return configError{Field: "io", Err: err}
|
|
||||||
}
|
|
||||||
config.IO = ioImpl
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cliConfig) fillWorkers(config *engine.Config) error {
|
|
||||||
config.Workers = c.Workers.Count
|
|
||||||
config.WorkerQueueSize = c.Workers.QueueSize
|
|
||||||
config.WorkerTCPMaxBufferedPagesTotal = c.Workers.TCPMaxBufferedPagesTotal
|
|
||||||
config.WorkerTCPMaxBufferedPagesPerConn = c.Workers.TCPMaxBufferedPagesPerConn
|
|
||||||
config.WorkerTCPTimeout = c.Workers.TCPTimeout
|
|
||||||
config.WorkerUDPMaxStreams = c.Workers.UDPMaxStreams
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config validates the fields and returns a ready-to-use engine config.
|
|
||||||
// This does not include the ruleset.
|
|
||||||
func (c *cliConfig) Config() (*engine.Config, error) {
|
|
||||||
engineConfig := &engine.Config{}
|
|
||||||
fillers := []func(*engine.Config) error{
|
|
||||||
c.fillLogger,
|
|
||||||
c.fillIO,
|
|
||||||
c.fillWorkers,
|
|
||||||
}
|
|
||||||
for _, f := range fillers {
|
|
||||||
if err := f(engineConfig); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return engineConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCPUCores() int {
|
|
||||||
// 尝试获取物理CPU核心数
|
|
||||||
if cpuInfo, err := cpu.Info(); err == nil && len(cpuInfo) > 0 {
|
|
||||||
// 获取第一个CPU的核心数
|
|
||||||
cores := int(cpuInfo[0].Cores)
|
|
||||||
if cores > 0 {
|
|
||||||
return cores
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果获取失败,使用runtime.NumCPU()作为后备
|
|
||||||
return runtime.NumCPU()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDefaultConfig() cliConfig {
|
|
||||||
cores := getCPUCores()
|
|
||||||
queueSize := cores * 64
|
|
||||||
if queueSize > 256 {
|
|
||||||
queueSize = 256
|
|
||||||
}
|
|
||||||
|
|
||||||
return cliConfig{
|
|
||||||
IO: cliConfigIO{
|
|
||||||
QueueSize: 1024,
|
|
||||||
QueueNum: nil,
|
|
||||||
Table: "",
|
|
||||||
ConnMarkAccept: 0,
|
|
||||||
ConnMarkDrop: 0,
|
|
||||||
ReadBuffer: 4194304,
|
|
||||||
WriteBuffer: 4194304,
|
|
||||||
Local: true,
|
|
||||||
RST: false,
|
|
||||||
},
|
|
||||||
Workers: cliConfigWorkers{
|
|
||||||
Count: cores,
|
|
||||||
QueueSize: queueSize,
|
|
||||||
TCPMaxBufferedPagesTotal: 65536,
|
|
||||||
TCPMaxBufferedPagesPerConn: 16,
|
|
||||||
TCPTimeout: 10 * time.Minute,
|
|
||||||
UDPMaxStreams: 4096,
|
|
||||||
},
|
|
||||||
Replay: cliConfigReplay{
|
|
||||||
Realtime: false,
|
|
||||||
},
|
|
||||||
Ruleset: cliConfigRuleset{
|
|
||||||
GeoIp: "",
|
|
||||||
GeoSite: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runMain(cmd *cobra.Command, args []string) {
|
|
||||||
// Config
|
|
||||||
config := getDefaultConfig()
|
|
||||||
|
|
||||||
// Try to read config file if specified, otherwise use defaults
|
|
||||||
if cfgFile != "" || viper.ConfigFileUsed() != "" {
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
|
||||||
logger.Warn("failed to read config file, using defaults", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
if err := viper.Unmarshal(&config); err != nil {
|
|
||||||
logger.Fatal("failed to parse config", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
engineConfig, err := config.Config()
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal("failed to parse config", zap.Error(err))
|
|
||||||
}
|
|
||||||
defer engineConfig.IO.Close() // Make sure to close IO on exit
|
|
||||||
|
|
||||||
// Ruleset
|
|
||||||
rawRs, err := ruleset.ExprRulesFromYAML(args[0])
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal("failed to load rules", zap.Error(err))
|
|
||||||
}
|
|
||||||
rsConfig := &ruleset.BuiltinConfig{
|
|
||||||
Logger: &rulesetLogger{},
|
|
||||||
GeoMatcher: geo.NewGeoMatcher(config.Ruleset.GeoSite, config.Ruleset.GeoIp),
|
|
||||||
ProtectedDialContext: engineConfig.IO.ProtectedDialContext,
|
|
||||||
}
|
|
||||||
rs, err := ruleset.CompileExprRules(rawRs, analyzers, modifiers, rsConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal("failed to compile rules", zap.Error(err))
|
|
||||||
}
|
|
||||||
engineConfig.Ruleset = rs
|
|
||||||
|
|
||||||
// Engine
|
|
||||||
en, err := engine.NewEngine(*engineConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal("failed to initialize engine", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal handling
|
|
||||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
||||||
go func() {
|
|
||||||
// Graceful shutdown
|
|
||||||
shutdownChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(shutdownChan, os.Interrupt, syscall.SIGTERM)
|
|
||||||
<-shutdownChan
|
|
||||||
logger.Info("shutting down gracefully...")
|
|
||||||
cancelFunc()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
// Rule reload
|
|
||||||
reloadChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(reloadChan, syscall.SIGHUP)
|
|
||||||
for {
|
|
||||||
<-reloadChan
|
|
||||||
logger.Info("reloading rules")
|
|
||||||
rawRs, err := ruleset.ExprRulesFromYAML(args[0])
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to load rules, using old rules", zap.Error(err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rs, err := ruleset.CompileExprRules(rawRs, analyzers, modifiers, rsConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to compile rules, using old rules", zap.Error(err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = en.UpdateRuleset(rs)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to update ruleset", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
logger.Info("rules reloaded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
logger.Info("engine started")
|
|
||||||
logger.Info("engine exited", zap.Error(en.Run(ctx)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type engineLogger struct{}
|
|
||||||
|
|
||||||
func (l *engineLogger) WorkerStart(id int) {
|
|
||||||
logger.Debug("worker started", zap.Int("id", id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) WorkerStop(id int) {
|
|
||||||
logger.Debug("worker stopped", zap.Int("id", id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) TCPStreamNew(workerID int, info ruleset.StreamInfo) {
|
|
||||||
logger.Debug("new TCP stream",
|
|
||||||
zap.Int("workerID", workerID),
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) TCPStreamPropUpdate(info ruleset.StreamInfo, close bool) {
|
|
||||||
logger.Debug("TCP stream property update",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.Any("props", info.Props),
|
|
||||||
zap.Bool("close", close))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) TCPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool) {
|
|
||||||
if noMatch {
|
|
||||||
logger.Debug("TCP stream no match",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.String("action", action.String()))
|
|
||||||
} else {
|
|
||||||
logger.Info("TCP stream action",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.String("action", action.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) TCPFlush(workerID, flushed, closed int) {
|
|
||||||
logger.Debug("TCP flush",
|
|
||||||
zap.Int("workerID", workerID),
|
|
||||||
zap.Int("flushed", flushed),
|
|
||||||
zap.Int("closed", closed))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) UDPStreamNew(workerID int, info ruleset.StreamInfo) {
|
|
||||||
logger.Debug("new UDP stream",
|
|
||||||
zap.Int("workerID", workerID),
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) UDPStreamPropUpdate(info ruleset.StreamInfo, close bool) {
|
|
||||||
logger.Debug("UDP stream property update",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.Any("props", info.Props),
|
|
||||||
zap.Bool("close", close))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool) {
|
|
||||||
if noMatch {
|
|
||||||
logger.Debug("UDP stream no match",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.String("action", action.String()))
|
|
||||||
} else {
|
|
||||||
logger.Info("UDP stream action",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.String("action", action.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) ModifyError(info ruleset.StreamInfo, err error) {
|
|
||||||
logger.Error("modify error",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) AnalyzerDebugf(streamID int64, name string, format string, args ...interface{}) {
|
|
||||||
logger.Debug("analyzer debug message",
|
|
||||||
zap.Int64("id", streamID),
|
|
||||||
zap.String("name", name),
|
|
||||||
zap.String("msg", fmt.Sprintf(format, args...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) AnalyzerInfof(streamID int64, name string, format string, args ...interface{}) {
|
|
||||||
logger.Info("analyzer info message",
|
|
||||||
zap.Int64("id", streamID),
|
|
||||||
zap.String("name", name),
|
|
||||||
zap.String("msg", fmt.Sprintf(format, args...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) AnalyzerErrorf(streamID int64, name string, format string, args ...interface{}) {
|
|
||||||
logger.Error("analyzer error message",
|
|
||||||
zap.Int64("id", streamID),
|
|
||||||
zap.String("name", name),
|
|
||||||
zap.String("msg", fmt.Sprintf(format, args...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type rulesetLogger struct{}
|
|
||||||
|
|
||||||
func (l *rulesetLogger) Log(info ruleset.StreamInfo, name string) {
|
|
||||||
logger.Info("ruleset log",
|
|
||||||
zap.String("name", name),
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.Any("props", info.Props))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *rulesetLogger) MatchError(info ruleset.StreamInfo, name string, err error) {
|
|
||||||
logger.Error("ruleset match error",
|
|
||||||
zap.String("name", name),
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func envOrDefaultString(key, def string) string {
|
|
||||||
if v := os.Getenv(key); v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/io"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Engine = (*engine)(nil)
|
|
||||||
|
|
||||||
type engine struct {
|
|
||||||
logger Logger
|
|
||||||
io io.PacketIO
|
|
||||||
workers []*worker
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEngine(config Config) (Engine, error) {
|
|
||||||
workerCount := config.Workers
|
|
||||||
if workerCount <= 0 {
|
|
||||||
workerCount = runtime.NumCPU()
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
workers := make([]*worker, workerCount)
|
|
||||||
for i := range workers {
|
|
||||||
workers[i], err = newWorker(workerConfig{
|
|
||||||
ID: i,
|
|
||||||
ChanSize: config.WorkerQueueSize,
|
|
||||||
Logger: config.Logger,
|
|
||||||
Ruleset: config.Ruleset,
|
|
||||||
TCPMaxBufferedPagesTotal: config.WorkerTCPMaxBufferedPagesTotal,
|
|
||||||
TCPMaxBufferedPagesPerConn: config.WorkerTCPMaxBufferedPagesPerConn,
|
|
||||||
TCPTimeout: config.WorkerTCPTimeout,
|
|
||||||
UDPMaxStreams: config.WorkerUDPMaxStreams,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &engine{
|
|
||||||
logger: config.Logger,
|
|
||||||
io: config.IO,
|
|
||||||
workers: workers,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *engine) UpdateRuleset(r ruleset.Ruleset) error {
|
|
||||||
for _, w := range e.workers {
|
|
||||||
if err := w.UpdateRuleset(r); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *engine) Run(ctx context.Context) error {
|
|
||||||
workerCtx, workerCancel := context.WithCancel(ctx)
|
|
||||||
defer workerCancel() // Stop workers
|
|
||||||
|
|
||||||
// Register IO shutdown
|
|
||||||
ioCtx, ioCancel := context.WithCancel(ctx)
|
|
||||||
e.io.SetCancelFunc(ioCancel)
|
|
||||||
defer ioCancel() // Stop IO
|
|
||||||
|
|
||||||
// Start workers
|
|
||||||
for _, w := range e.workers {
|
|
||||||
go w.Run(workerCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register IO callback
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
err := e.io.Register(ioCtx, func(p io.Packet, err error) bool {
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return e.dispatch(p)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block until IO errors or context is cancelled
|
|
||||||
select {
|
|
||||||
case err := <-errChan:
|
|
||||||
return err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
case <-ioCtx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispatch dispatches a packet to a worker.
|
|
||||||
func (e *engine) dispatch(p io.Packet) bool {
|
|
||||||
data := p.Data()
|
|
||||||
ipVersion := data[0] >> 4
|
|
||||||
var layerType gopacket.LayerType
|
|
||||||
if ipVersion == 4 {
|
|
||||||
layerType = layers.LayerTypeIPv4
|
|
||||||
} else if ipVersion == 6 {
|
|
||||||
layerType = layers.LayerTypeIPv6
|
|
||||||
} else {
|
|
||||||
// Unsupported network layer
|
|
||||||
_ = e.io.SetVerdict(p, io.VerdictAcceptStream, nil)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Convert to gopacket.Packet
|
|
||||||
packet := gopacket.NewPacket(data, layerType, gopacket.DecodeOptions{Lazy: true, NoCopy: true})
|
|
||||||
packet.Metadata().Timestamp = p.Timestamp()
|
|
||||||
// Load balance by stream ID
|
|
||||||
index := p.StreamID() % uint32(len(e.workers))
|
|
||||||
e.workers[index].Feed(&workerPacket{
|
|
||||||
StreamID: p.StreamID(),
|
|
||||||
Packet: packet,
|
|
||||||
SetVerdict: func(v io.Verdict, b []byte) error {
|
|
||||||
return e.io.SetVerdict(p, v, b)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/io"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Engine is the main engine for OpenGFW.
|
|
||||||
type Engine interface {
|
|
||||||
// UpdateRuleset updates the ruleset.
|
|
||||||
UpdateRuleset(ruleset.Ruleset) error
|
|
||||||
// Run runs the engine, until an error occurs or the context is cancelled.
|
|
||||||
Run(context.Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config is the configuration for the engine.
|
|
||||||
type Config struct {
|
|
||||||
Logger Logger
|
|
||||||
IO io.PacketIO
|
|
||||||
Ruleset ruleset.Ruleset
|
|
||||||
|
|
||||||
Workers int // Number of workers. Zero or negative means auto (number of CPU cores).
|
|
||||||
WorkerQueueSize int
|
|
||||||
WorkerTCPMaxBufferedPagesTotal int
|
|
||||||
WorkerTCPMaxBufferedPagesPerConn int
|
|
||||||
WorkerTCPTimeout time.Duration
|
|
||||||
WorkerUDPMaxStreams int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger is the combined logging interface for the engine, workers and analyzers.
|
|
||||||
type Logger interface {
|
|
||||||
WorkerStart(id int)
|
|
||||||
WorkerStop(id int)
|
|
||||||
|
|
||||||
TCPStreamNew(workerID int, info ruleset.StreamInfo)
|
|
||||||
TCPStreamPropUpdate(info ruleset.StreamInfo, close bool)
|
|
||||||
TCPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool)
|
|
||||||
TCPFlush(workerID, flushed, closed int)
|
|
||||||
|
|
||||||
UDPStreamNew(workerID int, info ruleset.StreamInfo)
|
|
||||||
UDPStreamPropUpdate(info ruleset.StreamInfo, close bool)
|
|
||||||
UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool)
|
|
||||||
|
|
||||||
ModifyError(info ruleset.StreamInfo, err error)
|
|
||||||
|
|
||||||
AnalyzerDebugf(streamID int64, name string, format string, args ...interface{})
|
|
||||||
AnalyzerInfof(streamID int64, name string, format string, args ...interface{})
|
|
||||||
AnalyzerErrorf(streamID int64, name string, format string, args ...interface{})
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/io"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/snowflake"
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
"github.com/google/gopacket/reassembly"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tcpVerdict is a subset of io.Verdict for TCP streams.
|
|
||||||
// We don't allow modifying or dropping a single packet
|
|
||||||
// for TCP streams for now, as it doesn't make much sense.
|
|
||||||
type tcpVerdict io.Verdict
|
|
||||||
|
|
||||||
const (
|
|
||||||
tcpVerdictAccept = tcpVerdict(io.VerdictAccept)
|
|
||||||
tcpVerdictAcceptStream = tcpVerdict(io.VerdictAcceptStream)
|
|
||||||
tcpVerdictDropStream = tcpVerdict(io.VerdictDropStream)
|
|
||||||
)
|
|
||||||
|
|
||||||
type tcpContext struct {
|
|
||||||
*gopacket.PacketMetadata
|
|
||||||
Verdict tcpVerdict
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *tcpContext) GetCaptureInfo() gopacket.CaptureInfo {
|
|
||||||
return ctx.CaptureInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
type tcpStreamFactory struct {
|
|
||||||
WorkerID int
|
|
||||||
Logger Logger
|
|
||||||
Node *snowflake.Node
|
|
||||||
|
|
||||||
RulesetMutex sync.RWMutex
|
|
||||||
Ruleset ruleset.Ruleset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *tcpStreamFactory) New(ipFlow, tcpFlow gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
|
|
||||||
id := f.Node.Generate()
|
|
||||||
ipSrc, ipDst := net.IP(ipFlow.Src().Raw()), net.IP(ipFlow.Dst().Raw())
|
|
||||||
info := ruleset.StreamInfo{
|
|
||||||
ID: id.Int64(),
|
|
||||||
Protocol: ruleset.ProtocolTCP,
|
|
||||||
SrcIP: ipSrc,
|
|
||||||
DstIP: ipDst,
|
|
||||||
SrcPort: uint16(tcp.SrcPort),
|
|
||||||
DstPort: uint16(tcp.DstPort),
|
|
||||||
Props: make(analyzer.CombinedPropMap),
|
|
||||||
}
|
|
||||||
f.Logger.TCPStreamNew(f.WorkerID, info)
|
|
||||||
f.RulesetMutex.RLock()
|
|
||||||
rs := f.Ruleset
|
|
||||||
f.RulesetMutex.RUnlock()
|
|
||||||
ans := analyzersToTCPAnalyzers(rs.Analyzers(info))
|
|
||||||
// Create entries for each analyzer
|
|
||||||
entries := make([]*tcpStreamEntry, 0, len(ans))
|
|
||||||
for _, a := range ans {
|
|
||||||
entries = append(entries, &tcpStreamEntry{
|
|
||||||
Name: a.Name(),
|
|
||||||
Stream: a.NewTCP(analyzer.TCPInfo{
|
|
||||||
SrcIP: ipSrc,
|
|
||||||
DstIP: ipDst,
|
|
||||||
SrcPort: uint16(tcp.SrcPort),
|
|
||||||
DstPort: uint16(tcp.DstPort),
|
|
||||||
}, &analyzerLogger{
|
|
||||||
StreamID: id.Int64(),
|
|
||||||
Name: a.Name(),
|
|
||||||
Logger: f.Logger,
|
|
||||||
}),
|
|
||||||
HasLimit: a.Limit() > 0,
|
|
||||||
Quota: a.Limit(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return &tcpStream{
|
|
||||||
info: info,
|
|
||||||
virgin: true,
|
|
||||||
logger: f.Logger,
|
|
||||||
ruleset: rs,
|
|
||||||
activeEntries: entries,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *tcpStreamFactory) UpdateRuleset(r ruleset.Ruleset) error {
|
|
||||||
f.RulesetMutex.Lock()
|
|
||||||
defer f.RulesetMutex.Unlock()
|
|
||||||
f.Ruleset = r
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type tcpStream struct {
|
|
||||||
info ruleset.StreamInfo
|
|
||||||
virgin bool // true if no packets have been processed
|
|
||||||
logger Logger
|
|
||||||
ruleset ruleset.Ruleset
|
|
||||||
activeEntries []*tcpStreamEntry
|
|
||||||
doneEntries []*tcpStreamEntry
|
|
||||||
lastVerdict tcpVerdict
|
|
||||||
}
|
|
||||||
|
|
||||||
type tcpStreamEntry struct {
|
|
||||||
Name string
|
|
||||||
Stream analyzer.TCPStream
|
|
||||||
HasLimit bool
|
|
||||||
Quota int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassembly.TCPFlowDirection, nextSeq reassembly.Sequence, start *bool, ac reassembly.AssemblerContext) bool {
|
|
||||||
if len(s.activeEntries) > 0 || s.virgin {
|
|
||||||
// Make sure every stream matches against the ruleset at least once,
|
|
||||||
// even if there are no activeEntries, as the ruleset may have built-in
|
|
||||||
// properties that need to be matched.
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
ctx := ac.(*tcpContext)
|
|
||||||
ctx.Verdict = s.lastVerdict
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.AssemblerContext) {
|
|
||||||
dir, start, end, skip := sg.Info()
|
|
||||||
rev := dir == reassembly.TCPDirServerToClient
|
|
||||||
avail, _ := sg.Lengths()
|
|
||||||
data := sg.Fetch(avail)
|
|
||||||
updated := false
|
|
||||||
for i := len(s.activeEntries) - 1; i >= 0; i-- {
|
|
||||||
// Important: reverse order so we can remove entries
|
|
||||||
entry := s.activeEntries[i]
|
|
||||||
update, closeUpdate, done := s.feedEntry(entry, rev, start, end, skip, data)
|
|
||||||
up1 := processPropUpdate(s.info.Props, entry.Name, update)
|
|
||||||
up2 := processPropUpdate(s.info.Props, entry.Name, closeUpdate)
|
|
||||||
updated = updated || up1 || up2
|
|
||||||
if done {
|
|
||||||
s.activeEntries = append(s.activeEntries[:i], s.activeEntries[i+1:]...)
|
|
||||||
s.doneEntries = append(s.doneEntries, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx := ac.(*tcpContext)
|
|
||||||
if updated || s.virgin {
|
|
||||||
s.virgin = false
|
|
||||||
s.logger.TCPStreamPropUpdate(s.info, false)
|
|
||||||
// Match properties against ruleset
|
|
||||||
result := s.ruleset.Match(s.info)
|
|
||||||
action := result.Action
|
|
||||||
if action != ruleset.ActionMaybe && action != ruleset.ActionModify {
|
|
||||||
verdict := actionToTCPVerdict(action)
|
|
||||||
s.lastVerdict = verdict
|
|
||||||
ctx.Verdict = verdict
|
|
||||||
s.logger.TCPStreamAction(s.info, action, false)
|
|
||||||
// Verdict issued, no need to process any more packets
|
|
||||||
s.closeActiveEntries()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s.activeEntries) == 0 && ctx.Verdict == tcpVerdictAccept {
|
|
||||||
// All entries are done but no verdict issued, accept stream
|
|
||||||
s.lastVerdict = tcpVerdictAcceptStream
|
|
||||||
ctx.Verdict = tcpVerdictAcceptStream
|
|
||||||
s.logger.TCPStreamAction(s.info, ruleset.ActionAllow, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tcpStream) ReassemblyComplete(ac reassembly.AssemblerContext) bool {
|
|
||||||
s.closeActiveEntries()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tcpStream) closeActiveEntries() {
|
|
||||||
// Signal close to all active entries & move them to doneEntries
|
|
||||||
updated := false
|
|
||||||
for _, entry := range s.activeEntries {
|
|
||||||
update := entry.Stream.Close(false)
|
|
||||||
up := processPropUpdate(s.info.Props, entry.Name, update)
|
|
||||||
updated = updated || up
|
|
||||||
}
|
|
||||||
if updated {
|
|
||||||
s.logger.TCPStreamPropUpdate(s.info, true)
|
|
||||||
}
|
|
||||||
s.doneEntries = append(s.doneEntries, s.activeEntries...)
|
|
||||||
s.activeEntries = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tcpStream) feedEntry(entry *tcpStreamEntry, rev, start, end bool, skip int, data []byte) (update *analyzer.PropUpdate, closeUpdate *analyzer.PropUpdate, done bool) {
|
|
||||||
if !entry.HasLimit {
|
|
||||||
update, done = entry.Stream.Feed(rev, start, end, skip, data)
|
|
||||||
} else {
|
|
||||||
qData := data
|
|
||||||
if len(qData) > entry.Quota {
|
|
||||||
qData = qData[:entry.Quota]
|
|
||||||
}
|
|
||||||
update, done = entry.Stream.Feed(rev, start, end, skip, qData)
|
|
||||||
entry.Quota -= len(qData)
|
|
||||||
if entry.Quota <= 0 {
|
|
||||||
// Quota exhausted, signal close & move to doneEntries
|
|
||||||
closeUpdate = entry.Stream.Close(true)
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzersToTCPAnalyzers(ans []analyzer.Analyzer) []analyzer.TCPAnalyzer {
|
|
||||||
tcpAns := make([]analyzer.TCPAnalyzer, 0, len(ans))
|
|
||||||
for _, a := range ans {
|
|
||||||
if tcpM, ok := a.(analyzer.TCPAnalyzer); ok {
|
|
||||||
tcpAns = append(tcpAns, tcpM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tcpAns
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionToTCPVerdict(a ruleset.Action) tcpVerdict {
|
|
||||||
switch a {
|
|
||||||
case ruleset.ActionMaybe, ruleset.ActionAllow, ruleset.ActionModify:
|
|
||||||
return tcpVerdictAcceptStream
|
|
||||||
case ruleset.ActionBlock, ruleset.ActionDrop:
|
|
||||||
return tcpVerdictDropStream
|
|
||||||
default:
|
|
||||||
// Should never happen
|
|
||||||
return tcpVerdictAcceptStream
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/io"
|
|
||||||
"github.com/apernet/OpenGFW/modifier"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/snowflake"
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// udpVerdict is a subset of io.Verdict for UDP streams.
|
|
||||||
// For UDP, we support all verdicts.
|
|
||||||
type udpVerdict io.Verdict
|
|
||||||
|
|
||||||
const (
|
|
||||||
udpVerdictAccept = udpVerdict(io.VerdictAccept)
|
|
||||||
udpVerdictAcceptModify = udpVerdict(io.VerdictAcceptModify)
|
|
||||||
udpVerdictAcceptStream = udpVerdict(io.VerdictAcceptStream)
|
|
||||||
udpVerdictDrop = udpVerdict(io.VerdictDrop)
|
|
||||||
udpVerdictDropStream = udpVerdict(io.VerdictDropStream)
|
|
||||||
)
|
|
||||||
|
|
||||||
var errInvalidModifier = errors.New("invalid modifier")
|
|
||||||
|
|
||||||
type udpContext struct {
|
|
||||||
Verdict udpVerdict
|
|
||||||
Packet []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type udpStreamFactory struct {
|
|
||||||
WorkerID int
|
|
||||||
Logger Logger
|
|
||||||
Node *snowflake.Node
|
|
||||||
|
|
||||||
RulesetMutex sync.RWMutex
|
|
||||||
Ruleset ruleset.Ruleset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *udpStreamFactory) New(ipFlow, udpFlow gopacket.Flow, udp *layers.UDP, uc *udpContext) *udpStream {
|
|
||||||
id := f.Node.Generate()
|
|
||||||
ipSrc, ipDst := net.IP(ipFlow.Src().Raw()), net.IP(ipFlow.Dst().Raw())
|
|
||||||
info := ruleset.StreamInfo{
|
|
||||||
ID: id.Int64(),
|
|
||||||
Protocol: ruleset.ProtocolUDP,
|
|
||||||
SrcIP: ipSrc,
|
|
||||||
DstIP: ipDst,
|
|
||||||
SrcPort: uint16(udp.SrcPort),
|
|
||||||
DstPort: uint16(udp.DstPort),
|
|
||||||
Props: make(analyzer.CombinedPropMap),
|
|
||||||
}
|
|
||||||
f.Logger.UDPStreamNew(f.WorkerID, info)
|
|
||||||
f.RulesetMutex.RLock()
|
|
||||||
rs := f.Ruleset
|
|
||||||
f.RulesetMutex.RUnlock()
|
|
||||||
ans := analyzersToUDPAnalyzers(rs.Analyzers(info))
|
|
||||||
// Create entries for each analyzer
|
|
||||||
entries := make([]*udpStreamEntry, 0, len(ans))
|
|
||||||
for _, a := range ans {
|
|
||||||
entries = append(entries, &udpStreamEntry{
|
|
||||||
Name: a.Name(),
|
|
||||||
Stream: a.NewUDP(analyzer.UDPInfo{
|
|
||||||
SrcIP: ipSrc,
|
|
||||||
DstIP: ipDst,
|
|
||||||
SrcPort: uint16(udp.SrcPort),
|
|
||||||
DstPort: uint16(udp.DstPort),
|
|
||||||
}, &analyzerLogger{
|
|
||||||
StreamID: id.Int64(),
|
|
||||||
Name: a.Name(),
|
|
||||||
Logger: f.Logger,
|
|
||||||
}),
|
|
||||||
HasLimit: a.Limit() > 0,
|
|
||||||
Quota: a.Limit(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return &udpStream{
|
|
||||||
info: info,
|
|
||||||
virgin: true,
|
|
||||||
logger: f.Logger,
|
|
||||||
ruleset: rs,
|
|
||||||
activeEntries: entries,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *udpStreamFactory) UpdateRuleset(r ruleset.Ruleset) error {
|
|
||||||
f.RulesetMutex.Lock()
|
|
||||||
defer f.RulesetMutex.Unlock()
|
|
||||||
f.Ruleset = r
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type udpStreamManager struct {
|
|
||||||
factory *udpStreamFactory
|
|
||||||
streams *lru.Cache[uint32, *udpStreamValue]
|
|
||||||
}
|
|
||||||
|
|
||||||
type udpStreamValue struct {
|
|
||||||
Stream *udpStream
|
|
||||||
IPFlow gopacket.Flow
|
|
||||||
UDPFlow gopacket.Flow
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *udpStreamValue) Match(ipFlow, udpFlow gopacket.Flow) (ok, rev bool) {
|
|
||||||
fwd := v.IPFlow == ipFlow && v.UDPFlow == udpFlow
|
|
||||||
rev = v.IPFlow == ipFlow.Reverse() && v.UDPFlow == udpFlow.Reverse()
|
|
||||||
return fwd || rev, rev
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUDPStreamManager(factory *udpStreamFactory, maxStreams int) (*udpStreamManager, error) {
|
|
||||||
ss, err := lru.New[uint32, *udpStreamValue](maxStreams)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &udpStreamManager{
|
|
||||||
factory: factory,
|
|
||||||
streams: ss,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *udpStreamManager) MatchWithContext(streamID uint32, ipFlow gopacket.Flow, udp *layers.UDP, uc *udpContext) {
|
|
||||||
rev := false
|
|
||||||
value, ok := m.streams.Get(streamID)
|
|
||||||
if !ok {
|
|
||||||
// New stream
|
|
||||||
value = &udpStreamValue{
|
|
||||||
Stream: m.factory.New(ipFlow, udp.TransportFlow(), udp, uc),
|
|
||||||
IPFlow: ipFlow,
|
|
||||||
UDPFlow: udp.TransportFlow(),
|
|
||||||
}
|
|
||||||
m.streams.Add(streamID, value)
|
|
||||||
} else {
|
|
||||||
// Stream ID exists, but is it really the same stream?
|
|
||||||
ok, rev = value.Match(ipFlow, udp.TransportFlow())
|
|
||||||
if !ok {
|
|
||||||
// It's not - close the old stream & replace it with a new one
|
|
||||||
value.Stream.Close()
|
|
||||||
value = &udpStreamValue{
|
|
||||||
Stream: m.factory.New(ipFlow, udp.TransportFlow(), udp, uc),
|
|
||||||
IPFlow: ipFlow,
|
|
||||||
UDPFlow: udp.TransportFlow(),
|
|
||||||
}
|
|
||||||
m.streams.Add(streamID, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if value.Stream.Accept(udp, rev, uc) {
|
|
||||||
value.Stream.Feed(udp, rev, uc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type udpStream struct {
|
|
||||||
info ruleset.StreamInfo
|
|
||||||
virgin bool // true if no packets have been processed
|
|
||||||
logger Logger
|
|
||||||
ruleset ruleset.Ruleset
|
|
||||||
activeEntries []*udpStreamEntry
|
|
||||||
doneEntries []*udpStreamEntry
|
|
||||||
lastVerdict udpVerdict
|
|
||||||
}
|
|
||||||
|
|
||||||
type udpStreamEntry struct {
|
|
||||||
Name string
|
|
||||||
Stream analyzer.UDPStream
|
|
||||||
HasLimit bool
|
|
||||||
Quota int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *udpStream) Accept(udp *layers.UDP, rev bool, uc *udpContext) bool {
|
|
||||||
if len(s.activeEntries) > 0 || s.virgin {
|
|
||||||
// Make sure every stream matches against the ruleset at least once,
|
|
||||||
// even if there are no activeEntries, as the ruleset may have built-in
|
|
||||||
// properties that need to be matched.
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
uc.Verdict = s.lastVerdict
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
|
|
||||||
updated := false
|
|
||||||
for i := len(s.activeEntries) - 1; i >= 0; i-- {
|
|
||||||
// Important: reverse order so we can remove entries
|
|
||||||
entry := s.activeEntries[i]
|
|
||||||
update, closeUpdate, done := s.feedEntry(entry, rev, udp.Payload)
|
|
||||||
up1 := processPropUpdate(s.info.Props, entry.Name, update)
|
|
||||||
up2 := processPropUpdate(s.info.Props, entry.Name, closeUpdate)
|
|
||||||
updated = updated || up1 || up2
|
|
||||||
if done {
|
|
||||||
s.activeEntries = append(s.activeEntries[:i], s.activeEntries[i+1:]...)
|
|
||||||
s.doneEntries = append(s.doneEntries, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if updated || s.virgin {
|
|
||||||
s.virgin = false
|
|
||||||
s.logger.UDPStreamPropUpdate(s.info, false)
|
|
||||||
// Match properties against ruleset
|
|
||||||
result := s.ruleset.Match(s.info)
|
|
||||||
action := result.Action
|
|
||||||
if action == ruleset.ActionModify {
|
|
||||||
// Call the modifier instance
|
|
||||||
udpMI, ok := result.ModInstance.(modifier.UDPModifierInstance)
|
|
||||||
if !ok {
|
|
||||||
// Not for UDP, fallback to maybe
|
|
||||||
s.logger.ModifyError(s.info, errInvalidModifier)
|
|
||||||
action = ruleset.ActionMaybe
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
uc.Packet, err = udpMI.Process(udp.Payload)
|
|
||||||
if err != nil {
|
|
||||||
// Modifier error, fallback to maybe
|
|
||||||
s.logger.ModifyError(s.info, err)
|
|
||||||
action = ruleset.ActionMaybe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if action != ruleset.ActionMaybe {
|
|
||||||
verdict, final := actionToUDPVerdict(action)
|
|
||||||
s.lastVerdict = verdict
|
|
||||||
uc.Verdict = verdict
|
|
||||||
s.logger.UDPStreamAction(s.info, action, false)
|
|
||||||
if final {
|
|
||||||
s.closeActiveEntries()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s.activeEntries) == 0 && uc.Verdict == udpVerdictAccept {
|
|
||||||
// All entries are done but no verdict issued, accept stream
|
|
||||||
s.lastVerdict = udpVerdictAcceptStream
|
|
||||||
uc.Verdict = udpVerdictAcceptStream
|
|
||||||
s.logger.UDPStreamAction(s.info, ruleset.ActionAllow, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *udpStream) Close() {
|
|
||||||
s.closeActiveEntries()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *udpStream) closeActiveEntries() {
|
|
||||||
// Signal close to all active entries & move them to doneEntries
|
|
||||||
updated := false
|
|
||||||
for _, entry := range s.activeEntries {
|
|
||||||
update := entry.Stream.Close(false)
|
|
||||||
up := processPropUpdate(s.info.Props, entry.Name, update)
|
|
||||||
updated = updated || up
|
|
||||||
}
|
|
||||||
if updated {
|
|
||||||
s.logger.UDPStreamPropUpdate(s.info, true)
|
|
||||||
}
|
|
||||||
s.doneEntries = append(s.doneEntries, s.activeEntries...)
|
|
||||||
s.activeEntries = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *udpStream) feedEntry(entry *udpStreamEntry, rev bool, data []byte) (update *analyzer.PropUpdate, closeUpdate *analyzer.PropUpdate, done bool) {
|
|
||||||
update, done = entry.Stream.Feed(rev, data)
|
|
||||||
if entry.HasLimit {
|
|
||||||
entry.Quota -= len(data)
|
|
||||||
if entry.Quota <= 0 {
|
|
||||||
// Quota exhausted, signal close & move to doneEntries
|
|
||||||
closeUpdate = entry.Stream.Close(true)
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzersToUDPAnalyzers(ans []analyzer.Analyzer) []analyzer.UDPAnalyzer {
|
|
||||||
udpAns := make([]analyzer.UDPAnalyzer, 0, len(ans))
|
|
||||||
for _, a := range ans {
|
|
||||||
if udpM, ok := a.(analyzer.UDPAnalyzer); ok {
|
|
||||||
udpAns = append(udpAns, udpM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return udpAns
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionToUDPVerdict(a ruleset.Action) (v udpVerdict, final bool) {
|
|
||||||
switch a {
|
|
||||||
case ruleset.ActionMaybe:
|
|
||||||
return udpVerdictAccept, false
|
|
||||||
case ruleset.ActionAllow:
|
|
||||||
return udpVerdictAcceptStream, true
|
|
||||||
case ruleset.ActionBlock:
|
|
||||||
return udpVerdictDropStream, true
|
|
||||||
case ruleset.ActionDrop:
|
|
||||||
return udpVerdictDrop, false
|
|
||||||
case ruleset.ActionModify:
|
|
||||||
return udpVerdictAcceptModify, false
|
|
||||||
default:
|
|
||||||
// Should never happen
|
|
||||||
return udpVerdictAccept, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package engine
|
|
||||||
|
|
||||||
import "github.com/apernet/OpenGFW/analyzer"
|
|
||||||
|
|
||||||
var _ analyzer.Logger = (*analyzerLogger)(nil)
|
|
||||||
|
|
||||||
type analyzerLogger struct {
|
|
||||||
StreamID int64
|
|
||||||
Name string
|
|
||||||
Logger Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *analyzerLogger) Debugf(format string, args ...interface{}) {
|
|
||||||
l.Logger.AnalyzerDebugf(l.StreamID, l.Name, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *analyzerLogger) Infof(format string, args ...interface{}) {
|
|
||||||
l.Logger.AnalyzerInfof(l.StreamID, l.Name, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *analyzerLogger) Errorf(format string, args ...interface{}) {
|
|
||||||
l.Logger.AnalyzerErrorf(l.StreamID, l.Name, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func processPropUpdate(cpm analyzer.CombinedPropMap, name string, update *analyzer.PropUpdate) (updated bool) {
|
|
||||||
if update == nil || update.Type == analyzer.PropUpdateNone {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch update.Type {
|
|
||||||
case analyzer.PropUpdateMerge:
|
|
||||||
m := cpm[name]
|
|
||||||
if m == nil {
|
|
||||||
m = make(analyzer.PropMap, len(update.M))
|
|
||||||
cpm[name] = m
|
|
||||||
}
|
|
||||||
for k, v := range update.M {
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case analyzer.PropUpdateReplace:
|
|
||||||
cpm[name] = update.M
|
|
||||||
return true
|
|
||||||
case analyzer.PropUpdateDelete:
|
|
||||||
delete(cpm, name)
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
// Invalid update type, ignore for now
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/io"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/snowflake"
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
"github.com/google/gopacket/reassembly"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultChanSize = 64
|
|
||||||
defaultTCPMaxBufferedPagesTotal = 65536
|
|
||||||
defaultTCPMaxBufferedPagesPerConnection = 16
|
|
||||||
defaultTCPTimeout = 10 * time.Minute
|
|
||||||
defaultUDPMaxStreams = 4096
|
|
||||||
|
|
||||||
tcpFlushInterval = 1 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
type workerPacket struct {
|
|
||||||
StreamID uint32
|
|
||||||
Packet gopacket.Packet
|
|
||||||
SetVerdict func(io.Verdict, []byte) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type worker struct {
|
|
||||||
id int
|
|
||||||
packetChan chan *workerPacket
|
|
||||||
logger Logger
|
|
||||||
|
|
||||||
tcpStreamFactory *tcpStreamFactory
|
|
||||||
tcpStreamPool *reassembly.StreamPool
|
|
||||||
tcpAssembler *reassembly.Assembler
|
|
||||||
tcpTimeout time.Duration
|
|
||||||
|
|
||||||
udpStreamFactory *udpStreamFactory
|
|
||||||
udpStreamManager *udpStreamManager
|
|
||||||
|
|
||||||
modSerializeBuffer gopacket.SerializeBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
type workerConfig struct {
|
|
||||||
ID int
|
|
||||||
ChanSize int
|
|
||||||
Logger Logger
|
|
||||||
Ruleset ruleset.Ruleset
|
|
||||||
TCPMaxBufferedPagesTotal int
|
|
||||||
TCPMaxBufferedPagesPerConn int
|
|
||||||
TCPTimeout time.Duration
|
|
||||||
UDPMaxStreams int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *workerConfig) fillDefaults() {
|
|
||||||
if c.ChanSize <= 0 {
|
|
||||||
c.ChanSize = defaultChanSize
|
|
||||||
}
|
|
||||||
if c.TCPMaxBufferedPagesTotal <= 0 {
|
|
||||||
c.TCPMaxBufferedPagesTotal = defaultTCPMaxBufferedPagesTotal
|
|
||||||
}
|
|
||||||
if c.TCPMaxBufferedPagesPerConn <= 0 {
|
|
||||||
c.TCPMaxBufferedPagesPerConn = defaultTCPMaxBufferedPagesPerConnection
|
|
||||||
}
|
|
||||||
if c.TCPTimeout <= 0 {
|
|
||||||
c.TCPTimeout = defaultTCPTimeout
|
|
||||||
}
|
|
||||||
if c.UDPMaxStreams <= 0 {
|
|
||||||
c.UDPMaxStreams = defaultUDPMaxStreams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWorker(config workerConfig) (*worker, error) {
|
|
||||||
config.fillDefaults()
|
|
||||||
sfNode, err := snowflake.NewNode(int64(config.ID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tcpSF := &tcpStreamFactory{
|
|
||||||
WorkerID: config.ID,
|
|
||||||
Logger: config.Logger,
|
|
||||||
Node: sfNode,
|
|
||||||
Ruleset: config.Ruleset,
|
|
||||||
}
|
|
||||||
tcpStreamPool := reassembly.NewStreamPool(tcpSF)
|
|
||||||
tcpAssembler := reassembly.NewAssembler(tcpStreamPool)
|
|
||||||
tcpAssembler.MaxBufferedPagesTotal = config.TCPMaxBufferedPagesTotal
|
|
||||||
tcpAssembler.MaxBufferedPagesPerConnection = config.TCPMaxBufferedPagesPerConn
|
|
||||||
udpSF := &udpStreamFactory{
|
|
||||||
WorkerID: config.ID,
|
|
||||||
Logger: config.Logger,
|
|
||||||
Node: sfNode,
|
|
||||||
Ruleset: config.Ruleset,
|
|
||||||
}
|
|
||||||
udpSM, err := newUDPStreamManager(udpSF, config.UDPMaxStreams)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &worker{
|
|
||||||
id: config.ID,
|
|
||||||
packetChan: make(chan *workerPacket, config.ChanSize),
|
|
||||||
logger: config.Logger,
|
|
||||||
tcpStreamFactory: tcpSF,
|
|
||||||
tcpStreamPool: tcpStreamPool,
|
|
||||||
tcpAssembler: tcpAssembler,
|
|
||||||
tcpTimeout: config.TCPTimeout,
|
|
||||||
udpStreamFactory: udpSF,
|
|
||||||
udpStreamManager: udpSM,
|
|
||||||
modSerializeBuffer: gopacket.NewSerializeBuffer(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *worker) Feed(p *workerPacket) {
|
|
||||||
w.packetChan <- p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *worker) Run(ctx context.Context) {
|
|
||||||
w.logger.WorkerStart(w.id)
|
|
||||||
defer w.logger.WorkerStop(w.id)
|
|
||||||
|
|
||||||
tcpFlushTicker := time.NewTicker(tcpFlushInterval)
|
|
||||||
defer tcpFlushTicker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case wPkt := <-w.packetChan:
|
|
||||||
if wPkt == nil {
|
|
||||||
// Closed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v, b := w.handle(wPkt.StreamID, wPkt.Packet)
|
|
||||||
_ = wPkt.SetVerdict(v, b)
|
|
||||||
case <-tcpFlushTicker.C:
|
|
||||||
w.flushTCP(w.tcpTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *worker) UpdateRuleset(r ruleset.Ruleset) error {
|
|
||||||
if err := w.tcpStreamFactory.UpdateRuleset(r); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return w.udpStreamFactory.UpdateRuleset(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *worker) handle(streamID uint32, p gopacket.Packet) (io.Verdict, []byte) {
|
|
||||||
netLayer, trLayer := p.NetworkLayer(), p.TransportLayer()
|
|
||||||
if netLayer == nil || trLayer == nil {
|
|
||||||
// Invalid packet
|
|
||||||
return io.VerdictAccept, nil
|
|
||||||
}
|
|
||||||
ipFlow := netLayer.NetworkFlow()
|
|
||||||
switch tr := trLayer.(type) {
|
|
||||||
case *layers.TCP:
|
|
||||||
return w.handleTCP(ipFlow, p.Metadata(), tr), nil
|
|
||||||
case *layers.UDP:
|
|
||||||
v, modPayload := w.handleUDP(streamID, ipFlow, tr)
|
|
||||||
if v == io.VerdictAcceptModify && modPayload != nil {
|
|
||||||
tr.Payload = modPayload
|
|
||||||
_ = tr.SetNetworkLayerForChecksum(netLayer)
|
|
||||||
_ = w.modSerializeBuffer.Clear()
|
|
||||||
err := gopacket.SerializePacket(w.modSerializeBuffer,
|
|
||||||
gopacket.SerializeOptions{
|
|
||||||
FixLengths: true,
|
|
||||||
ComputeChecksums: true,
|
|
||||||
}, p)
|
|
||||||
if err != nil {
|
|
||||||
// Just accept without modification for now
|
|
||||||
return io.VerdictAccept, nil
|
|
||||||
}
|
|
||||||
return v, w.modSerializeBuffer.Bytes()
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
// Unsupported protocol
|
|
||||||
return io.VerdictAccept, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *worker) handleTCP(ipFlow gopacket.Flow, pMeta *gopacket.PacketMetadata, tcp *layers.TCP) io.Verdict {
|
|
||||||
ctx := &tcpContext{
|
|
||||||
PacketMetadata: pMeta,
|
|
||||||
Verdict: tcpVerdictAccept,
|
|
||||||
}
|
|
||||||
w.tcpAssembler.AssembleWithContext(ipFlow, tcp, ctx)
|
|
||||||
return io.Verdict(ctx.Verdict)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *worker) flushTCP(timeout time.Duration) {
|
|
||||||
flushed, closed := w.tcpAssembler.FlushCloseOlderThan(time.Now().Add(-timeout))
|
|
||||||
w.logger.TCPFlush(w.id, flushed, closed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *worker) handleUDP(streamID uint32, ipFlow gopacket.Flow, udp *layers.UDP) (io.Verdict, []byte) {
|
|
||||||
ctx := &udpContext{
|
|
||||||
Verdict: udpVerdictAccept,
|
|
||||||
}
|
|
||||||
w.udpStreamManager.MatchWithContext(streamID, ipFlow, udp, ctx)
|
|
||||||
return io.Verdict(ctx.Verdict), ctx.Packet
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
module github.com/apernet/OpenGFW
|
|
||||||
|
|
||||||
go 1.21
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/bwmarrin/snowflake v0.3.0
|
|
||||||
github.com/coreos/go-iptables v0.7.0
|
|
||||||
github.com/expr-lang/expr v1.16.3
|
|
||||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf
|
|
||||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
|
||||||
github.com/mdlayher/netlink v1.6.0
|
|
||||||
github.com/quic-go/quic-go v0.41.0
|
|
||||||
github.com/shirou/gopsutil/v3 v3.24.1
|
|
||||||
github.com/spf13/cobra v1.8.0
|
|
||||||
github.com/spf13/viper v1.18.2
|
|
||||||
go.uber.org/zap v1.26.0
|
|
||||||
golang.org/x/crypto v0.19.0
|
|
||||||
golang.org/x/sys v0.17.0
|
|
||||||
google.golang.org/protobuf v1.31.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
||||||
github.com/josharian/native v1.0.0 // indirect
|
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
|
||||||
github.com/mdlayher/socket v0.1.1 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
|
||||||
golang.org/x/net v0.19.0 // indirect
|
|
||||||
golang.org/x/sync v0.5.0 // indirect
|
|
||||||
golang.org/x/text v0.14.0 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
|
||||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
|
||||||
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
|
||||||
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/expr-lang/expr v1.16.3 h1:NLldf786GffptcXNxxJx5dQ+FzeWDKChBDqOOwyK8to=
|
|
||||||
github.com/expr-lang/expr v1.16.3/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
|
||||||
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/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/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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
||||||
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/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.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
|
||||||
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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866 h1:NaJi58bCZZh0jjPw78EqDZekPEfhlzYE01C5R+zh1tE=
|
|
||||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866/go.mod h1:riddUzxTSBpJXk3qBHtYr4qOhFhT6k/1c0E3qkQjQpA=
|
|
||||||
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/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
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/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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
|
||||||
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/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
|
|
||||||
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
|
||||||
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/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
|
||||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
|
||||||
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/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
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/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
|
||||||
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
|
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
|
||||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|
||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
|
||||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
|
||||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
|
||||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
|
||||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
|
||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
|
||||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
|
||||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
|
||||||
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
|
||||||
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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
|
||||||
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-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/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
|
||||||
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-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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
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/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.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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
|
||||||
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 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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package io
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Verdict int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// VerdictAccept accepts the packet, but continues to process the stream.
|
|
||||||
VerdictAccept Verdict = iota
|
|
||||||
// VerdictAcceptModify is like VerdictAccept, but replaces the packet with a new one.
|
|
||||||
VerdictAcceptModify
|
|
||||||
// VerdictAcceptStream accepts the packet and stops processing the stream.
|
|
||||||
VerdictAcceptStream
|
|
||||||
// VerdictDrop drops the packet, but does not block the stream.
|
|
||||||
VerdictDrop
|
|
||||||
// VerdictDropStream drops the packet and blocks the stream.
|
|
||||||
VerdictDropStream
|
|
||||||
)
|
|
||||||
|
|
||||||
// Packet represents an IP packet.
|
|
||||||
type Packet interface {
|
|
||||||
// StreamID is the ID of the stream the packet belongs to.
|
|
||||||
StreamID() uint32
|
|
||||||
// Timestamp is the time the packet was received.
|
|
||||||
Timestamp() time.Time
|
|
||||||
// Data is the raw packet data, starting with the IP header.
|
|
||||||
Data() []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// PacketCallback is called for each packet received.
|
|
||||||
// Return false to "unregister" and stop receiving packets.
|
|
||||||
type PacketCallback func(Packet, error) bool
|
|
||||||
|
|
||||||
type PacketIO interface {
|
|
||||||
// Register registers a callback to be called for each packet received.
|
|
||||||
// The callback should be called in one or more separate goroutines,
|
|
||||||
// and stop when the context is cancelled.
|
|
||||||
Register(context.Context, PacketCallback) error
|
|
||||||
// SetVerdict sets the verdict for a packet.
|
|
||||||
SetVerdict(Packet, Verdict, []byte) error
|
|
||||||
// ProtectedDialContext is like net.DialContext, but the connection is "protected"
|
|
||||||
// in the sense that the packets sent/received through the connection must bypass
|
|
||||||
// the packet IO and not be processed by the callback.
|
|
||||||
ProtectedDialContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
||||||
// Close closes the packet IO.
|
|
||||||
Close() error
|
|
||||||
// SetCancelFunc gives packet IO access to context cancel function, enabling it to
|
|
||||||
// trigger a shutdown
|
|
||||||
SetCancelFunc(cancelFunc context.CancelFunc) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrInvalidPacket struct {
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrInvalidPacket) Error() string {
|
|
||||||
return "invalid packet: " + e.Err.Error()
|
|
||||||
}
|
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
package io
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
|
||||||
"github.com/florianl/go-nfqueue"
|
|
||||||
"github.com/mdlayher/netlink"
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
nfqueueDefaultQueueNum = 100
|
|
||||||
nfqueueMaxPacketLen = 0xFFFF
|
|
||||||
nfqueueDefaultQueueSize = 128
|
|
||||||
|
|
||||||
nfqueueDefaultConnMarkAccept = 1001
|
|
||||||
|
|
||||||
nftFamily = "inet"
|
|
||||||
nftDefaultTable = "opengfw"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) generateNftRules() (*nftTableSpec, error) {
|
|
||||||
if n.local && n.rst {
|
|
||||||
return nil, errors.New("tcp rst is not supported in local mode")
|
|
||||||
}
|
|
||||||
table := &nftTableSpec{
|
|
||||||
Family: nftFamily,
|
|
||||||
Table: n.table,
|
|
||||||
}
|
|
||||||
table.Defines = append(table.Defines, fmt.Sprintf("define ACCEPT_CTMARK=%d", n.connMarkAccept))
|
|
||||||
table.Defines = append(table.Defines, fmt.Sprintf("define DROP_CTMARK=%d", n.connMarkDrop))
|
|
||||||
table.Defines = append(table.Defines, fmt.Sprintf("define QUEUE_NUM=%d", n.queueNum))
|
|
||||||
if n.local {
|
|
||||||
table.Chains = []nftChainSpec{
|
|
||||||
{Chain: "INPUT", Header: "type filter hook input priority filter; policy accept;"},
|
|
||||||
{Chain: "OUTPUT", Header: "type filter hook output priority filter; policy accept;"},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
table.Chains = []nftChainSpec{
|
|
||||||
{Chain: "FORWARD", Header: "type filter hook forward priority filter; policy accept;"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := range table.Chains {
|
|
||||||
c := &table.Chains[i]
|
|
||||||
c.Rules = append(c.Rules, "meta mark $ACCEPT_CTMARK ct mark set $ACCEPT_CTMARK") // Bypass protected connections
|
|
||||||
c.Rules = append(c.Rules, "ct mark $ACCEPT_CTMARK counter accept")
|
|
||||||
if n.rst {
|
|
||||||
c.Rules = append(c.Rules, "ip protocol tcp ct mark $DROP_CTMARK counter reject with tcp reset")
|
|
||||||
}
|
|
||||||
c.Rules = append(c.Rules, "ct mark $DROP_CTMARK counter drop")
|
|
||||||
c.Rules = append(c.Rules, "counter queue num $QUEUE_NUM bypass")
|
|
||||||
}
|
|
||||||
return table, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) generateIptRules() ([]iptRule, error) {
|
|
||||||
if n.local && n.rst {
|
|
||||||
return nil, errors.New("tcp rst is not supported in local mode")
|
|
||||||
}
|
|
||||||
var chains []string
|
|
||||||
if n.local {
|
|
||||||
chains = []string{"INPUT", "OUTPUT"}
|
|
||||||
} else {
|
|
||||||
chains = []string{"FORWARD"}
|
|
||||||
}
|
|
||||||
rules := make([]iptRule, 0, 4*len(chains))
|
|
||||||
for _, chain := range chains {
|
|
||||||
// Bypass protected connections
|
|
||||||
rules = append(rules, iptRule{"filter", chain, []string{"-m", "mark", "--mark", strconv.Itoa(n.connMarkAccept), "-j", "CONNMARK", "--set-mark", strconv.Itoa(n.connMarkAccept)}})
|
|
||||||
rules = append(rules, iptRule{"filter", chain, []string{"-m", "connmark", "--mark", strconv.Itoa(n.connMarkAccept), "-j", "ACCEPT"}})
|
|
||||||
if n.rst {
|
|
||||||
rules = append(rules, iptRule{"filter", chain, []string{"-p", "tcp", "-m", "connmark", "--mark", strconv.Itoa(n.connMarkDrop), "-j", "REJECT", "--reject-with", "tcp-reset"}})
|
|
||||||
}
|
|
||||||
rules = append(rules, iptRule{"filter", chain, []string{"-m", "connmark", "--mark", strconv.Itoa(n.connMarkDrop), "-j", "DROP"}})
|
|
||||||
rules = append(rules, iptRule{"filter", chain, []string{"-j", "NFQUEUE", "--queue-num", strconv.Itoa(n.queueNum), "--queue-bypass"}})
|
|
||||||
}
|
|
||||||
|
|
||||||
return rules, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ PacketIO = (*nfqueuePacketIO)(nil)
|
|
||||||
|
|
||||||
var errNotNFQueuePacket = errors.New("not an NFQueue packet")
|
|
||||||
|
|
||||||
type nfqueuePacketIO struct {
|
|
||||||
n *nfqueue.Nfqueue
|
|
||||||
local bool
|
|
||||||
rst bool
|
|
||||||
rSet bool // whether the nftables/iptables rules have been set
|
|
||||||
queueNum int
|
|
||||||
table string // nftable name
|
|
||||||
connMarkAccept int
|
|
||||||
connMarkDrop int
|
|
||||||
|
|
||||||
// iptables not nil = use iptables instead of nftables
|
|
||||||
ipt4 *iptables.IPTables
|
|
||||||
ipt6 *iptables.IPTables
|
|
||||||
|
|
||||||
protectedDialer *net.Dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
type NFQueuePacketIOConfig struct {
|
|
||||||
QueueSize uint32
|
|
||||||
QueueNum *uint16
|
|
||||||
Table string
|
|
||||||
ConnMarkAccept uint32
|
|
||||||
ConnMarkDrop uint32
|
|
||||||
|
|
||||||
ReadBuffer int
|
|
||||||
WriteBuffer int
|
|
||||||
Local bool
|
|
||||||
RST bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
|
|
||||||
if config.QueueSize == 0 {
|
|
||||||
config.QueueSize = nfqueueDefaultQueueSize
|
|
||||||
}
|
|
||||||
if config.QueueNum == nil {
|
|
||||||
queueNum := uint16(nfqueueDefaultQueueNum)
|
|
||||||
config.QueueNum = &queueNum
|
|
||||||
}
|
|
||||||
if config.Table == "" {
|
|
||||||
config.Table = nftDefaultTable
|
|
||||||
}
|
|
||||||
if config.ConnMarkAccept == 0 {
|
|
||||||
config.ConnMarkAccept = nfqueueDefaultConnMarkAccept
|
|
||||||
}
|
|
||||||
if config.ConnMarkDrop == 0 {
|
|
||||||
config.ConnMarkDrop = config.ConnMarkAccept + 1
|
|
||||||
if config.ConnMarkDrop == 0 {
|
|
||||||
// Overflow
|
|
||||||
config.ConnMarkDrop = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.ConnMarkAccept == config.ConnMarkDrop {
|
|
||||||
return nil, errors.New("connMarkAccept and connMarkDrop cannot be the same")
|
|
||||||
}
|
|
||||||
var ipt4, ipt6 *iptables.IPTables
|
|
||||||
var err error
|
|
||||||
if nftCheck() != nil {
|
|
||||||
// We prefer nftables, but if it's not available, fall back to iptables
|
|
||||||
ipt4, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n, err := nfqueue.Open(&nfqueue.Config{
|
|
||||||
NfQueue: *config.QueueNum,
|
|
||||||
MaxPacketLen: nfqueueMaxPacketLen,
|
|
||||||
MaxQueueLen: config.QueueSize,
|
|
||||||
Copymode: nfqueue.NfQnlCopyPacket,
|
|
||||||
Flags: nfqueue.NfQaCfgFlagConntrack,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if config.ReadBuffer > 0 {
|
|
||||||
err = n.Con.SetReadBuffer(config.ReadBuffer)
|
|
||||||
if err != nil {
|
|
||||||
_ = n.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.WriteBuffer > 0 {
|
|
||||||
err = n.Con.SetWriteBuffer(config.WriteBuffer)
|
|
||||||
if err != nil {
|
|
||||||
_ = n.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &nfqueuePacketIO{
|
|
||||||
n: n,
|
|
||||||
local: config.Local,
|
|
||||||
rst: config.RST,
|
|
||||||
queueNum: int(*config.QueueNum),
|
|
||||||
table: config.Table,
|
|
||||||
connMarkAccept: int(config.ConnMarkAccept),
|
|
||||||
connMarkDrop: int(config.ConnMarkDrop),
|
|
||||||
ipt4: ipt4,
|
|
||||||
ipt6: ipt6,
|
|
||||||
protectedDialer: &net.Dialer{
|
|
||||||
Control: func(network, address string, c syscall.RawConn) error {
|
|
||||||
var err error
|
|
||||||
cErr := c.Control(func(fd uintptr) {
|
|
||||||
err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(config.ConnMarkAccept))
|
|
||||||
})
|
|
||||||
if cErr != nil {
|
|
||||||
return cErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error {
|
|
||||||
err := n.n.RegisterWithErrorFunc(ctx,
|
|
||||||
func(a nfqueue.Attribute) int {
|
|
||||||
if ok, verdict := n.packetAttributeSanityCheck(a); !ok {
|
|
||||||
if a.PacketID != nil {
|
|
||||||
_ = n.n.SetVerdict(*a.PacketID, verdict)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
p := &nfqueuePacket{
|
|
||||||
id: *a.PacketID,
|
|
||||||
streamID: ctIDFromCtBytes(*a.Ct),
|
|
||||||
data: *a.Payload,
|
|
||||||
}
|
|
||||||
// Use timestamp from attribute if available, otherwise use current time as fallback
|
|
||||||
if a.Timestamp != nil {
|
|
||||||
p.timestamp = *a.Timestamp
|
|
||||||
} else {
|
|
||||||
p.timestamp = time.Now()
|
|
||||||
}
|
|
||||||
return okBoolToInt(cb(p, nil))
|
|
||||||
},
|
|
||||||
func(e error) int {
|
|
||||||
if opErr := (*netlink.OpError)(nil); errors.As(e, &opErr) {
|
|
||||||
if errors.Is(opErr.Err, unix.ENOBUFS) {
|
|
||||||
// Kernel buffer temporarily full, ignore
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return okBoolToInt(cb(nil, e))
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !n.rSet {
|
|
||||||
if n.ipt4 != nil {
|
|
||||||
err = n.setupIpt(false)
|
|
||||||
} else {
|
|
||||||
err = n.setupNft(false)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n.rSet = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) packetAttributeSanityCheck(a nfqueue.Attribute) (ok bool, verdict int) {
|
|
||||||
if a.PacketID == nil {
|
|
||||||
// Re-inject to NFQUEUE is actually not possible in this condition
|
|
||||||
return false, -1
|
|
||||||
}
|
|
||||||
if a.Payload == nil || len(*a.Payload) < 20 {
|
|
||||||
// 20 is the minimum possible size of an IP packet
|
|
||||||
return false, nfqueue.NfDrop
|
|
||||||
}
|
|
||||||
if a.Ct == nil {
|
|
||||||
// Multicast packets may not have a conntrack, but only appear in local mode
|
|
||||||
if n.local {
|
|
||||||
return false, nfqueue.NfAccept
|
|
||||||
}
|
|
||||||
return false, nfqueue.NfDrop
|
|
||||||
}
|
|
||||||
return true, -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) SetVerdict(p Packet, v Verdict, newPacket []byte) error {
|
|
||||||
nP, ok := p.(*nfqueuePacket)
|
|
||||||
if !ok {
|
|
||||||
return &ErrInvalidPacket{Err: errNotNFQueuePacket}
|
|
||||||
}
|
|
||||||
switch v {
|
|
||||||
case VerdictAccept:
|
|
||||||
return n.n.SetVerdict(nP.id, nfqueue.NfAccept)
|
|
||||||
case VerdictAcceptModify:
|
|
||||||
return n.n.SetVerdictModPacket(nP.id, nfqueue.NfAccept, newPacket)
|
|
||||||
case VerdictAcceptStream:
|
|
||||||
return n.n.SetVerdictWithConnMark(nP.id, nfqueue.NfAccept, n.connMarkAccept)
|
|
||||||
case VerdictDrop:
|
|
||||||
return n.n.SetVerdict(nP.id, nfqueue.NfDrop)
|
|
||||||
case VerdictDropStream:
|
|
||||||
return n.n.SetVerdictWithConnMark(nP.id, nfqueue.NfDrop, n.connMarkDrop)
|
|
||||||
default:
|
|
||||||
// Invalid verdict, ignore for now
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) ProtectedDialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
return n.protectedDialer.DialContext(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) Close() error {
|
|
||||||
if n.rSet {
|
|
||||||
if n.ipt4 != nil {
|
|
||||||
_ = n.setupIpt(true)
|
|
||||||
} else {
|
|
||||||
_ = n.setupNft(true)
|
|
||||||
}
|
|
||||||
n.rSet = false
|
|
||||||
}
|
|
||||||
return n.n.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// nfqueue IO does not issue shutdown
|
|
||||||
func (n *nfqueuePacketIO) SetCancelFunc(cancelFunc context.CancelFunc) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) setupNft(remove bool) error {
|
|
||||||
rules, err := n.generateNftRules()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rulesText := rules.String()
|
|
||||||
if remove {
|
|
||||||
err = nftDelete(nftFamily, n.table)
|
|
||||||
} else {
|
|
||||||
// Delete first to make sure no leftover rules
|
|
||||||
_ = nftDelete(nftFamily, n.table)
|
|
||||||
err = nftAdd(rulesText)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) setupIpt(remove bool) error {
|
|
||||||
rules, err := n.generateIptRules()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if remove {
|
|
||||||
err = iptsBatchDeleteIfExists([]*iptables.IPTables{n.ipt4, n.ipt6}, rules)
|
|
||||||
} else {
|
|
||||||
err = iptsBatchAppendUnique([]*iptables.IPTables{n.ipt4, n.ipt6}, rules)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Packet = (*nfqueuePacket)(nil)
|
|
||||||
|
|
||||||
type nfqueuePacket struct {
|
|
||||||
id uint32
|
|
||||||
streamID uint32
|
|
||||||
timestamp time.Time
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *nfqueuePacket) StreamID() uint32 {
|
|
||||||
return p.streamID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *nfqueuePacket) Timestamp() time.Time {
|
|
||||||
return p.timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *nfqueuePacket) Data() []byte {
|
|
||||||
return p.data
|
|
||||||
}
|
|
||||||
|
|
||||||
func okBoolToInt(ok bool) int {
|
|
||||||
if ok {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func nftCheck() error {
|
|
||||||
_, err := exec.LookPath("nft")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func nftAdd(input string) error {
|
|
||||||
cmd := exec.Command("nft", "-f", "-")
|
|
||||||
cmd.Stdin = strings.NewReader(input)
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func nftDelete(family, table string) error {
|
|
||||||
cmd := exec.Command("nft", "delete", "table", family, table)
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
type nftTableSpec struct {
|
|
||||||
Defines []string
|
|
||||||
Family, Table string
|
|
||||||
Chains []nftChainSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *nftTableSpec) String() string {
|
|
||||||
chains := make([]string, 0, len(t.Chains))
|
|
||||||
for _, c := range t.Chains {
|
|
||||||
chains = append(chains, c.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
%s
|
|
||||||
|
|
||||||
table %s %s {
|
|
||||||
%s
|
|
||||||
}
|
|
||||||
`, strings.Join(t.Defines, "\n"), t.Family, t.Table, strings.Join(chains, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
type nftChainSpec struct {
|
|
||||||
Chain string
|
|
||||||
Header string
|
|
||||||
Rules []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *nftChainSpec) String() string {
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
chain %s {
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
}
|
|
||||||
`, c.Chain, c.Header, strings.Join(c.Rules, "\n\x20\x20\x20\x20"))
|
|
||||||
}
|
|
||||||
|
|
||||||
type iptRule struct {
|
|
||||||
Table, Chain string
|
|
||||||
RuleSpec []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func iptsBatchAppendUnique(ipts []*iptables.IPTables, rules []iptRule) error {
|
|
||||||
for _, r := range rules {
|
|
||||||
for _, ipt := range ipts {
|
|
||||||
err := ipt.AppendUnique(r.Table, r.Chain, r.RuleSpec...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func iptsBatchDeleteIfExists(ipts []*iptables.IPTables, rules []iptRule) error {
|
|
||||||
for _, r := range rules {
|
|
||||||
for _, ipt := range ipts {
|
|
||||||
err := ipt.DeleteIfExists(r.Table, r.Chain, r.RuleSpec...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ctIDFromCtBytes(ct []byte) uint32 {
|
|
||||||
ctAttrs, err := netlink.UnmarshalAttributes(ct)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
for _, attr := range ctAttrs {
|
|
||||||
if attr.Type == 12 { // CTA_ID
|
|
||||||
return binary.BigEndian.Uint32(attr.Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
package io
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"hash/crc32"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/pcapgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ PacketIO = (*pcapPacketIO)(nil)
|
|
||||||
|
|
||||||
type pcapPacketIO struct {
|
|
||||||
pcapFile io.ReadCloser
|
|
||||||
pcap *pcapgo.Reader
|
|
||||||
timeOffset *time.Duration
|
|
||||||
ioCancel context.CancelFunc
|
|
||||||
config PcapPacketIOConfig
|
|
||||||
|
|
||||||
dialer *net.Dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
type PcapPacketIOConfig struct {
|
|
||||||
PcapFile string
|
|
||||||
Realtime bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPcapPacketIO(config PcapPacketIOConfig) (PacketIO, error) {
|
|
||||||
pcapFile, err := os.Open(config.PcapFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
handle, err := pcapgo.NewReader(pcapFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &pcapPacketIO{
|
|
||||||
pcapFile: pcapFile,
|
|
||||||
pcap: handle,
|
|
||||||
timeOffset: nil,
|
|
||||||
ioCancel: nil,
|
|
||||||
config: config,
|
|
||||||
dialer: &net.Dialer{},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pcapPacketIO) Register(ctx context.Context, cb PacketCallback) error {
|
|
||||||
go func() {
|
|
||||||
packetSource := gopacket.NewPacketSource(p.pcap, p.pcap.LinkType())
|
|
||||||
for packet := range packetSource.Packets() {
|
|
||||||
p.wait(packet)
|
|
||||||
|
|
||||||
networkLayer := packet.NetworkLayer()
|
|
||||||
if networkLayer != nil {
|
|
||||||
src, dst := networkLayer.NetworkFlow().Endpoints()
|
|
||||||
endpoints := []string{src.String(), dst.String()}
|
|
||||||
sort.Strings(endpoints)
|
|
||||||
id := crc32.Checksum([]byte(strings.Join(endpoints, ",")), crc32.IEEETable)
|
|
||||||
|
|
||||||
cb(&pcapPacket{
|
|
||||||
streamID: id,
|
|
||||||
timestamp: packet.Metadata().Timestamp,
|
|
||||||
data: packet.LinkLayer().LayerPayload(),
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Give the workers a chance to finish everything
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
// Stop the engine when all packets are finished
|
|
||||||
p.ioCancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// A normal dialer is sufficient as pcap IO does not mess up with the networking
|
|
||||||
func (p *pcapPacketIO) ProtectedDialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
return p.dialer.DialContext(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pcapPacketIO) SetVerdict(pkt Packet, v Verdict, newPacket []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pcapPacketIO) SetCancelFunc(cancelFunc context.CancelFunc) error {
|
|
||||||
p.ioCancel = cancelFunc
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pcapPacketIO) Close() error {
|
|
||||||
return p.pcapFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intentionally slow down the replay
|
|
||||||
// In realtime mode, this is to match the timestamps in the capture
|
|
||||||
func (p *pcapPacketIO) wait(packet gopacket.Packet) {
|
|
||||||
if !p.config.Realtime {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.timeOffset == nil {
|
|
||||||
offset := time.Since(packet.Metadata().Timestamp)
|
|
||||||
p.timeOffset = &offset
|
|
||||||
} else {
|
|
||||||
t := time.Until(packet.Metadata().Timestamp.Add(*p.timeOffset))
|
|
||||||
time.Sleep(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Packet = (*pcapPacket)(nil)
|
|
||||||
|
|
||||||
type pcapPacket struct {
|
|
||||||
streamID uint32
|
|
||||||
timestamp time.Time
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pcapPacket) StreamID() uint32 {
|
|
||||||
return p.streamID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pcapPacket) Timestamp() time.Time {
|
|
||||||
return p.timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pcapPacket) Data() []byte {
|
|
||||||
return p.data
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package modifier
|
|
||||||
|
|
||||||
type Modifier interface {
|
|
||||||
// Name returns the name of the modifier.
|
|
||||||
Name() string
|
|
||||||
// New returns a new modifier instance.
|
|
||||||
New(args map[string]interface{}) (Instance, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Instance interface{}
|
|
||||||
|
|
||||||
type UDPModifierInstance interface {
|
|
||||||
Instance
|
|
||||||
// Process takes a UDP packet and returns a modified UDP packet.
|
|
||||||
Process(data []byte) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrInvalidPacket struct {
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrInvalidPacket) Error() string {
|
|
||||||
return "invalid packet: " + e.Err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrInvalidArgs struct {
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrInvalidArgs) Error() string {
|
|
||||||
return "invalid args: " + e.Err.Error()
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package udp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/modifier"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ modifier.Modifier = (*DNSModifier)(nil)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalidIP = errors.New("invalid ip")
|
|
||||||
errNotValidDNSResponse = errors.New("not a valid dns response")
|
|
||||||
errEmptyDNSQuestion = errors.New("empty dns question")
|
|
||||||
)
|
|
||||||
|
|
||||||
type DNSModifier struct{}
|
|
||||||
|
|
||||||
func (m *DNSModifier) Name() string {
|
|
||||||
return "dns"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DNSModifier) New(args map[string]interface{}) (modifier.Instance, error) {
|
|
||||||
i := &dnsModifierInstance{}
|
|
||||||
aStr, ok := args["a"].(string)
|
|
||||||
if ok {
|
|
||||||
a := net.ParseIP(aStr).To4()
|
|
||||||
if a == nil {
|
|
||||||
return nil, &modifier.ErrInvalidArgs{Err: errInvalidIP}
|
|
||||||
}
|
|
||||||
i.A = a
|
|
||||||
}
|
|
||||||
aaaaStr, ok := args["aaaa"].(string)
|
|
||||||
if ok {
|
|
||||||
aaaa := net.ParseIP(aaaaStr).To16()
|
|
||||||
if aaaa == nil {
|
|
||||||
return nil, &modifier.ErrInvalidArgs{Err: errInvalidIP}
|
|
||||||
}
|
|
||||||
i.AAAA = aaaa
|
|
||||||
}
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ modifier.UDPModifierInstance = (*dnsModifierInstance)(nil)
|
|
||||||
|
|
||||||
type dnsModifierInstance struct {
|
|
||||||
A net.IP
|
|
||||||
AAAA net.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dnsModifierInstance) Process(data []byte) ([]byte, error) {
|
|
||||||
dns := &layers.DNS{}
|
|
||||||
err := dns.DecodeFromBytes(data, gopacket.NilDecodeFeedback)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &modifier.ErrInvalidPacket{Err: err}
|
|
||||||
}
|
|
||||||
if !dns.QR || dns.ResponseCode != layers.DNSResponseCodeNoErr {
|
|
||||||
return nil, &modifier.ErrInvalidPacket{Err: errNotValidDNSResponse}
|
|
||||||
}
|
|
||||||
if len(dns.Questions) == 0 {
|
|
||||||
return nil, &modifier.ErrInvalidPacket{Err: errEmptyDNSQuestion}
|
|
||||||
}
|
|
||||||
// In practice, most if not all DNS clients only send one question
|
|
||||||
// per packet, so we don't care about the rest for now.
|
|
||||||
q := dns.Questions[0]
|
|
||||||
switch q.Type {
|
|
||||||
case layers.DNSTypeA:
|
|
||||||
if i.A != nil {
|
|
||||||
dns.Answers = []layers.DNSResourceRecord{{
|
|
||||||
Name: q.Name,
|
|
||||||
Type: layers.DNSTypeA,
|
|
||||||
Class: layers.DNSClassIN,
|
|
||||||
IP: i.A,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
case layers.DNSTypeAAAA:
|
|
||||||
if i.AAAA != nil {
|
|
||||||
dns.Answers = []layers.DNSResourceRecord{{
|
|
||||||
Name: q.Name,
|
|
||||||
Type: layers.DNSTypeAAAA,
|
|
||||||
Class: layers.DNSClassIN,
|
|
||||||
IP: i.AAAA,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf := gopacket.NewSerializeBuffer() // Modifiers must be safe for concurrent use, so we can't reuse the buffer
|
|
||||||
err = gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
|
|
||||||
FixLengths: true,
|
|
||||||
ComputeChecksums: true,
|
|
||||||
}, dns)
|
|
||||||
return buf.Bytes(), err
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package builtins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MatchCIDR(ip string, cidr *net.IPNet) bool {
|
|
||||||
ipAddr := net.ParseIP(ip)
|
|
||||||
if ipAddr == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return cidr.Contains(ipAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompileCIDR(cidr string) (*net.IPNet, error) {
|
|
||||||
_, ipNet, err := net.ParseCIDR(cidr)
|
|
||||||
return ipNet, err
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package geo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
geoipFilename = "geoip.dat"
|
|
||||||
geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
|
|
||||||
geositeFilename = "geosite.dat"
|
|
||||||
geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
|
|
||||||
|
|
||||||
geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ GeoLoader = (*V2GeoLoader)(nil)
|
|
||||||
|
|
||||||
// V2GeoLoader provides the on-demand GeoIP/MatchGeoSite database
|
|
||||||
// loading functionality required by the ACL engine.
|
|
||||||
// Empty filenames = automatic download from built-in URLs.
|
|
||||||
type V2GeoLoader struct {
|
|
||||||
GeoIPFilename string
|
|
||||||
GeoSiteFilename string
|
|
||||||
UpdateInterval time.Duration
|
|
||||||
|
|
||||||
DownloadFunc func(filename, url string)
|
|
||||||
DownloadErrFunc func(err error)
|
|
||||||
|
|
||||||
geoipMap map[string]*v2geo.GeoIP
|
|
||||||
geositeMap map[string]*v2geo.GeoSite
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDefaultGeoLoader(geoSiteFilename, geoIpFilename string) *V2GeoLoader {
|
|
||||||
return &V2GeoLoader{
|
|
||||||
GeoIPFilename: geoIpFilename,
|
|
||||||
GeoSiteFilename: geoSiteFilename,
|
|
||||||
DownloadFunc: func(filename, url string) {},
|
|
||||||
DownloadErrFunc: func(err error) {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *V2GeoLoader) shouldDownload(filename string) bool {
|
|
||||||
info, err := os.Stat(filename)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
dt := time.Since(info.ModTime())
|
|
||||||
if l.UpdateInterval == 0 {
|
|
||||||
return dt > geoDefaultUpdateInterval
|
|
||||||
} else {
|
|
||||||
return dt > l.UpdateInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *V2GeoLoader) download(filename, url string) error {
|
|
||||||
l.DownloadFunc(filename, url)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
l.DownloadErrFunc(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
f, err := os.Create(filename)
|
|
||||||
if err != nil {
|
|
||||||
l.DownloadErrFunc(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(f, resp.Body)
|
|
||||||
l.DownloadErrFunc(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *V2GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
|
|
||||||
if l.geoipMap != nil {
|
|
||||||
return l.geoipMap, nil
|
|
||||||
}
|
|
||||||
autoDL := false
|
|
||||||
filename := l.GeoIPFilename
|
|
||||||
if filename == "" {
|
|
||||||
autoDL = true
|
|
||||||
filename = geoipFilename
|
|
||||||
}
|
|
||||||
if autoDL && l.shouldDownload(filename) {
|
|
||||||
err := l.download(filename, geoipURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m, err := v2geo.LoadGeoIP(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
l.geoipMap = m
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *V2GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
|
|
||||||
if l.geositeMap != nil {
|
|
||||||
return l.geositeMap, nil
|
|
||||||
}
|
|
||||||
autoDL := false
|
|
||||||
filename := l.GeoSiteFilename
|
|
||||||
if filename == "" {
|
|
||||||
autoDL = true
|
|
||||||
filename = geositeFilename
|
|
||||||
}
|
|
||||||
if autoDL && l.shouldDownload(filename) {
|
|
||||||
err := l.download(filename, geositeURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m, err := v2geo.LoadGeoSite(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
l.geositeMap = m
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package geo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GeoMatcher struct {
|
|
||||||
geoLoader GeoLoader
|
|
||||||
geoSiteMatcher map[string]hostMatcher
|
|
||||||
siteMatcherLock sync.Mutex
|
|
||||||
geoIpMatcher map[string]hostMatcher
|
|
||||||
ipMatcherLock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGeoMatcher(geoSiteFilename, geoIpFilename string) *GeoMatcher {
|
|
||||||
return &GeoMatcher{
|
|
||||||
geoLoader: NewDefaultGeoLoader(geoSiteFilename, geoIpFilename),
|
|
||||||
geoSiteMatcher: make(map[string]hostMatcher),
|
|
||||||
geoIpMatcher: make(map[string]hostMatcher),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GeoMatcher) MatchGeoIp(ip, condition string) bool {
|
|
||||||
g.ipMatcherLock.Lock()
|
|
||||||
defer g.ipMatcherLock.Unlock()
|
|
||||||
|
|
||||||
matcher, ok := g.geoIpMatcher[condition]
|
|
||||||
if !ok {
|
|
||||||
// GeoIP matcher
|
|
||||||
condition = strings.ToLower(condition)
|
|
||||||
country := condition
|
|
||||||
if len(country) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
gMap, err := g.geoLoader.LoadGeoIP()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
list, ok := gMap[country]
|
|
||||||
if !ok || list == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
matcher, err = newGeoIPMatcher(list)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
g.geoIpMatcher[condition] = matcher
|
|
||||||
}
|
|
||||||
parseIp := net.ParseIP(ip)
|
|
||||||
if parseIp == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ipv4 := parseIp.To4()
|
|
||||||
if ipv4 != nil {
|
|
||||||
return matcher.Match(HostInfo{IPv4: ipv4})
|
|
||||||
}
|
|
||||||
ipv6 := parseIp.To16()
|
|
||||||
if ipv6 != nil {
|
|
||||||
return matcher.Match(HostInfo{IPv6: ipv6})
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GeoMatcher) MatchGeoSite(site, condition string) bool {
|
|
||||||
g.siteMatcherLock.Lock()
|
|
||||||
defer g.siteMatcherLock.Unlock()
|
|
||||||
|
|
||||||
matcher, ok := g.geoSiteMatcher[condition]
|
|
||||||
if !ok {
|
|
||||||
// MatchGeoSite matcher
|
|
||||||
condition = strings.ToLower(condition)
|
|
||||||
name, attrs := parseGeoSiteName(condition)
|
|
||||||
if len(name) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
gMap, err := g.geoLoader.LoadGeoSite()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
list, ok := gMap[name]
|
|
||||||
if !ok || list == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
matcher, err = newGeositeMatcher(list, attrs)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
g.geoSiteMatcher[condition] = matcher
|
|
||||||
}
|
|
||||||
return matcher.Match(HostInfo{Name: site})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GeoMatcher) LoadGeoSite() error {
|
|
||||||
_, err := g.geoLoader.LoadGeoSite()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GeoMatcher) LoadGeoIP() error {
|
|
||||||
_, err := g.geoLoader.LoadGeoIP()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseGeoSiteName(s string) (string, []string) {
|
|
||||||
parts := strings.Split(s, "@")
|
|
||||||
base := strings.TrimSpace(parts[0])
|
|
||||||
attrs := parts[1:]
|
|
||||||
for i := range attrs {
|
|
||||||
attrs[i] = strings.TrimSpace(attrs[i])
|
|
||||||
}
|
|
||||||
return base, attrs
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package geo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HostInfo struct {
|
|
||||||
Name string
|
|
||||||
IPv4 net.IP
|
|
||||||
IPv6 net.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HostInfo) String() string {
|
|
||||||
return fmt.Sprintf("%s|%s|%s", h.Name, h.IPv4, h.IPv6)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeoLoader interface {
|
|
||||||
LoadGeoIP() (map[string]*v2geo.GeoIP, error)
|
|
||||||
LoadGeoSite() (map[string]*v2geo.GeoSite, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type hostMatcher interface {
|
|
||||||
Match(HostInfo) bool
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package geo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ hostMatcher = (*geoipMatcher)(nil)
|
|
||||||
|
|
||||||
type geoipMatcher struct {
|
|
||||||
N4 []*net.IPNet // sorted
|
|
||||||
N6 []*net.IPNet // sorted
|
|
||||||
Inverse bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchIP tries to match the given IP address with the corresponding IPNets.
|
|
||||||
// Note that this function does NOT handle the Inverse flag.
|
|
||||||
func (m *geoipMatcher) matchIP(ip net.IP) bool {
|
|
||||||
var n []*net.IPNet
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
// N4 stores IPv4 addresses in 4-byte form.
|
|
||||||
// Make sure we use it here too, otherwise bytes.Compare will fail.
|
|
||||||
ip = ip4
|
|
||||||
n = m.N4
|
|
||||||
} else {
|
|
||||||
n = m.N6
|
|
||||||
}
|
|
||||||
left, right := 0, len(n)-1
|
|
||||||
for left <= right {
|
|
||||||
mid := (left + right) / 2
|
|
||||||
if n[mid].Contains(ip) {
|
|
||||||
return true
|
|
||||||
} else if bytes.Compare(n[mid].IP, ip) < 0 {
|
|
||||||
left = mid + 1
|
|
||||||
} else {
|
|
||||||
right = mid - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *geoipMatcher) Match(host HostInfo) bool {
|
|
||||||
if host.IPv4 != nil {
|
|
||||||
if m.matchIP(host.IPv4) {
|
|
||||||
return !m.Inverse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if host.IPv6 != nil {
|
|
||||||
if m.matchIP(host.IPv6) {
|
|
||||||
return !m.Inverse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m.Inverse
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGeoIPMatcher(list *v2geo.GeoIP) (*geoipMatcher, error) {
|
|
||||||
n4 := make([]*net.IPNet, 0)
|
|
||||||
n6 := make([]*net.IPNet, 0)
|
|
||||||
for _, cidr := range list.Cidr {
|
|
||||||
if len(cidr.Ip) == 4 {
|
|
||||||
// IPv4
|
|
||||||
n4 = append(n4, &net.IPNet{
|
|
||||||
IP: cidr.Ip,
|
|
||||||
Mask: net.CIDRMask(int(cidr.Prefix), 32),
|
|
||||||
})
|
|
||||||
} else if len(cidr.Ip) == 16 {
|
|
||||||
// IPv6
|
|
||||||
n6 = append(n6, &net.IPNet{
|
|
||||||
IP: cidr.Ip,
|
|
||||||
Mask: net.CIDRMask(int(cidr.Prefix), 128),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("invalid IP length")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort the IPNets, so we can do binary search later.
|
|
||||||
sort.Slice(n4, func(i, j int) bool {
|
|
||||||
return bytes.Compare(n4[i].IP, n4[j].IP) < 0
|
|
||||||
})
|
|
||||||
sort.Slice(n6, func(i, j int) bool {
|
|
||||||
return bytes.Compare(n6[i].IP, n6[j].IP) < 0
|
|
||||||
})
|
|
||||||
return &geoipMatcher{
|
|
||||||
N4: n4,
|
|
||||||
N6: n6,
|
|
||||||
Inverse: list.InverseMatch,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ hostMatcher = (*geositeMatcher)(nil)
|
|
||||||
|
|
||||||
type geositeDomainType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
geositeDomainPlain geositeDomainType = iota
|
|
||||||
geositeDomainRegex
|
|
||||||
geositeDomainRoot
|
|
||||||
geositeDomainFull
|
|
||||||
)
|
|
||||||
|
|
||||||
type geositeDomain struct {
|
|
||||||
Type geositeDomainType
|
|
||||||
Value string
|
|
||||||
Regex *regexp.Regexp
|
|
||||||
Attrs map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type geositeMatcher struct {
|
|
||||||
Domains []geositeDomain
|
|
||||||
// Attributes are matched using "and" logic - if you have multiple attributes here,
|
|
||||||
// a domain must have all of those attributes to be considered a match.
|
|
||||||
Attrs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *geositeMatcher) matchDomain(domain geositeDomain, host HostInfo) bool {
|
|
||||||
// Match attributes first
|
|
||||||
if len(m.Attrs) > 0 {
|
|
||||||
if len(domain.Attrs) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, attr := range m.Attrs {
|
|
||||||
if !domain.Attrs[attr] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch domain.Type {
|
|
||||||
case geositeDomainPlain:
|
|
||||||
return strings.Contains(host.Name, domain.Value)
|
|
||||||
case geositeDomainRegex:
|
|
||||||
if domain.Regex != nil {
|
|
||||||
return domain.Regex.MatchString(host.Name)
|
|
||||||
}
|
|
||||||
case geositeDomainFull:
|
|
||||||
return host.Name == domain.Value
|
|
||||||
case geositeDomainRoot:
|
|
||||||
if host.Name == domain.Value {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return strings.HasSuffix(host.Name, "."+domain.Value)
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *geositeMatcher) Match(host HostInfo) bool {
|
|
||||||
for _, domain := range m.Domains {
|
|
||||||
if m.matchDomain(domain, host) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGeositeMatcher(list *v2geo.GeoSite, attrs []string) (*geositeMatcher, error) {
|
|
||||||
domains := make([]geositeDomain, len(list.Domain))
|
|
||||||
for i, domain := range list.Domain {
|
|
||||||
switch domain.Type {
|
|
||||||
case v2geo.Domain_Plain:
|
|
||||||
domains[i] = geositeDomain{
|
|
||||||
Type: geositeDomainPlain,
|
|
||||||
Value: domain.Value,
|
|
||||||
Attrs: domainAttributeToMap(domain.Attribute),
|
|
||||||
}
|
|
||||||
case v2geo.Domain_Regex:
|
|
||||||
regex, err := regexp.Compile(domain.Value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
domains[i] = geositeDomain{
|
|
||||||
Type: geositeDomainRegex,
|
|
||||||
Regex: regex,
|
|
||||||
Attrs: domainAttributeToMap(domain.Attribute),
|
|
||||||
}
|
|
||||||
case v2geo.Domain_Full:
|
|
||||||
domains[i] = geositeDomain{
|
|
||||||
Type: geositeDomainFull,
|
|
||||||
Value: domain.Value,
|
|
||||||
Attrs: domainAttributeToMap(domain.Attribute),
|
|
||||||
}
|
|
||||||
case v2geo.Domain_RootDomain:
|
|
||||||
domains[i] = geositeDomain{
|
|
||||||
Type: geositeDomainRoot,
|
|
||||||
Value: domain.Value,
|
|
||||||
Attrs: domainAttributeToMap(domain.Attribute),
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unsupported domain type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &geositeMatcher{
|
|
||||||
Domains: domains,
|
|
||||||
Attrs: attrs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func domainAttributeToMap(attrs []*v2geo.Domain_Attribute) map[string]bool {
|
|
||||||
m := make(map[string]bool)
|
|
||||||
for _, attr := range attrs {
|
|
||||||
// Supposedly there are also int attributes,
|
|
||||||
// but nobody seems to use them, so we treat everything as boolean for now.
|
|
||||||
m[attr.Key] = true
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package v2geo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoadGeoIP loads a GeoIP data file and converts it to a map.
|
|
||||||
// The keys of the map (country codes) are all normalized to lowercase.
|
|
||||||
func LoadGeoIP(filename string) (map[string]*GeoIP, error) {
|
|
||||||
bs, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var list GeoIPList
|
|
||||||
if err := proto.Unmarshal(bs, &list); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m := make(map[string]*GeoIP)
|
|
||||||
for _, entry := range list.Entry {
|
|
||||||
m[strings.ToLower(entry.CountryCode)] = entry
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadGeoSite loads a GeoSite data file and converts it to a map.
|
|
||||||
// The keys of the map (site keys) are all normalized to lowercase.
|
|
||||||
func LoadGeoSite(filename string) (map[string]*GeoSite, error) {
|
|
||||||
bs, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var list GeoSiteList
|
|
||||||
if err := proto.Unmarshal(bs, &list); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m := make(map[string]*GeoSite)
|
|
||||||
for _, entry := range list.Entry {
|
|
||||||
m[strings.ToLower(entry.CountryCode)] = entry
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
@@ -1,745 +0,0 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// protoc-gen-go v1.31.0
|
|
||||||
// protoc v4.24.4
|
|
||||||
// source: v2geo.proto
|
|
||||||
|
|
||||||
package v2geo
|
|
||||||
|
|
||||||
import (
|
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
|
||||||
reflect "reflect"
|
|
||||||
sync "sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Verify that this generated code is sufficiently up-to-date.
|
|
||||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
|
||||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
|
||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Type of domain value.
|
|
||||||
type Domain_Type int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
// The value is used as is.
|
|
||||||
Domain_Plain Domain_Type = 0
|
|
||||||
// The value is used as a regular expression.
|
|
||||||
Domain_Regex Domain_Type = 1
|
|
||||||
// The value is a root domain.
|
|
||||||
Domain_RootDomain Domain_Type = 2
|
|
||||||
// The value is a domain.
|
|
||||||
Domain_Full Domain_Type = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enum value maps for Domain_Type.
|
|
||||||
var (
|
|
||||||
Domain_Type_name = map[int32]string{
|
|
||||||
0: "Plain",
|
|
||||||
1: "Regex",
|
|
||||||
2: "RootDomain",
|
|
||||||
3: "Full",
|
|
||||||
}
|
|
||||||
Domain_Type_value = map[string]int32{
|
|
||||||
"Plain": 0,
|
|
||||||
"Regex": 1,
|
|
||||||
"RootDomain": 2,
|
|
||||||
"Full": 3,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (x Domain_Type) Enum() *Domain_Type {
|
|
||||||
p := new(Domain_Type)
|
|
||||||
*p = x
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x Domain_Type) String() string {
|
|
||||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Domain_Type) Descriptor() protoreflect.EnumDescriptor {
|
|
||||||
return file_v2geo_proto_enumTypes[0].Descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Domain_Type) Type() protoreflect.EnumType {
|
|
||||||
return &file_v2geo_proto_enumTypes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x Domain_Type) Number() protoreflect.EnumNumber {
|
|
||||||
return protoreflect.EnumNumber(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use Domain_Type.Descriptor instead.
|
|
||||||
func (Domain_Type) EnumDescriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain for routing decision.
|
|
||||||
type Domain struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
// Domain matching type.
|
|
||||||
Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Domain_Type" json:"type,omitempty"`
|
|
||||||
// Domain value.
|
|
||||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
|
||||||
// Attributes of this domain. May be used for filtering.
|
|
||||||
Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain) Reset() {
|
|
||||||
*x = Domain{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[0]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Domain) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *Domain) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[0]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
|
|
||||||
func (*Domain) Descriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{0}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain) GetType() Domain_Type {
|
|
||||||
if x != nil {
|
|
||||||
return x.Type
|
|
||||||
}
|
|
||||||
return Domain_Plain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain) GetValue() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Value
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain) GetAttribute() []*Domain_Attribute {
|
|
||||||
if x != nil {
|
|
||||||
return x.Attribute
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP for routing decision, in CIDR form.
|
|
||||||
type CIDR struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
// IP address, should be either 4 or 16 bytes.
|
|
||||||
Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
|
|
||||||
// Number of leading ones in the network mask.
|
|
||||||
Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CIDR) Reset() {
|
|
||||||
*x = CIDR{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[1]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CIDR) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*CIDR) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *CIDR) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[1]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use CIDR.ProtoReflect.Descriptor instead.
|
|
||||||
func (*CIDR) Descriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{1}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CIDR) GetIp() []byte {
|
|
||||||
if x != nil {
|
|
||||||
return x.Ip
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CIDR) GetPrefix() uint32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.Prefix
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeoIP struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
|
|
||||||
Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"`
|
|
||||||
InverseMatch bool `protobuf:"varint,3,opt,name=inverse_match,json=inverseMatch,proto3" json:"inverse_match,omitempty"`
|
|
||||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
|
||||||
ResourceHash []byte `protobuf:"bytes,4,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
|
|
||||||
Code string `protobuf:"bytes,5,opt,name=code,proto3" json:"code,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIP) Reset() {
|
|
||||||
*x = GeoIP{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[2]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIP) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*GeoIP) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *GeoIP) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[2]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
|
|
||||||
func (*GeoIP) Descriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{2}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIP) GetCountryCode() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.CountryCode
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIP) GetCidr() []*CIDR {
|
|
||||||
if x != nil {
|
|
||||||
return x.Cidr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIP) GetInverseMatch() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.InverseMatch
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIP) GetResourceHash() []byte {
|
|
||||||
if x != nil {
|
|
||||||
return x.ResourceHash
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIP) GetCode() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Code
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeoIPList struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIPList) Reset() {
|
|
||||||
*x = GeoIPList{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[3]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIPList) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*GeoIPList) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[3]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
|
|
||||||
func (*GeoIPList) Descriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{3}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoIPList) GetEntry() []*GeoIP {
|
|
||||||
if x != nil {
|
|
||||||
return x.Entry
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeoSite struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
|
|
||||||
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
|
||||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
|
||||||
ResourceHash []byte `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
|
|
||||||
Code string `protobuf:"bytes,4,opt,name=code,proto3" json:"code,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSite) Reset() {
|
|
||||||
*x = GeoSite{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[4]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSite) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*GeoSite) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *GeoSite) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[4]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use MatchGeoSite.ProtoReflect.Descriptor instead.
|
|
||||||
func (*GeoSite) Descriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{4}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSite) GetCountryCode() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.CountryCode
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSite) GetDomain() []*Domain {
|
|
||||||
if x != nil {
|
|
||||||
return x.Domain
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSite) GetResourceHash() []byte {
|
|
||||||
if x != nil {
|
|
||||||
return x.ResourceHash
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSite) GetCode() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Code
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeoSiteList struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSiteList) Reset() {
|
|
||||||
*x = GeoSiteList{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[5]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSiteList) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*GeoSiteList) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *GeoSiteList) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[5]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead.
|
|
||||||
func (*GeoSiteList) Descriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{5}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GeoSiteList) GetEntry() []*GeoSite {
|
|
||||||
if x != nil {
|
|
||||||
return x.Entry
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Domain_Attribute struct {
|
|
||||||
state protoimpl.MessageState
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
|
|
||||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
|
||||||
// Types that are assignable to TypedValue:
|
|
||||||
//
|
|
||||||
// *Domain_Attribute_BoolValue
|
|
||||||
// *Domain_Attribute_IntValue
|
|
||||||
TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain_Attribute) Reset() {
|
|
||||||
*x = Domain_Attribute{}
|
|
||||||
if protoimpl.UnsafeEnabled {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[6]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain_Attribute) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Domain_Attribute) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_v2geo_proto_msgTypes[6]
|
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead.
|
|
||||||
func (*Domain_Attribute) Descriptor() ([]byte, []int) {
|
|
||||||
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain_Attribute) GetKey() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Key
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {
|
|
||||||
if m != nil {
|
|
||||||
return m.TypedValue
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain_Attribute) GetBoolValue() bool {
|
|
||||||
if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok {
|
|
||||||
return x.BoolValue
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Domain_Attribute) GetIntValue() int64 {
|
|
||||||
if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok {
|
|
||||||
return x.IntValue
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type isDomain_Attribute_TypedValue interface {
|
|
||||||
isDomain_Attribute_TypedValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Domain_Attribute_BoolValue struct {
|
|
||||||
BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Domain_Attribute_IntValue struct {
|
|
||||||
IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}
|
|
||||||
|
|
||||||
func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}
|
|
||||||
|
|
||||||
var File_v2geo_proto protoreflect.FileDescriptor
|
|
||||||
|
|
||||||
var file_v2geo_proto_rawDesc = []byte{
|
|
||||||
0x0a, 0x0b, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x02,
|
|
||||||
0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
|
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e,
|
|
||||||
0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
|
|
||||||
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
|
||||||
0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20,
|
|
||||||
0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74,
|
|
||||||
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
|
|
||||||
0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10,
|
|
||||||
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
|
|
||||||
0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
|
|
||||||
0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75,
|
|
||||||
0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03,
|
|
||||||
0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65,
|
|
||||||
0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22,
|
|
||||||
0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e,
|
|
||||||
0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0e, 0x0a,
|
|
||||||
0x0a, 0x52, 0x6f, 0x6f, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a,
|
|
||||||
0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12,
|
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12,
|
|
||||||
0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
|
||||||
0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xa3, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49,
|
|
||||||
0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64,
|
|
||||||
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79,
|
|
||||||
0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03,
|
|
||||||
0x28, 0x0b, 0x32, 0x05, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12,
|
|
||||||
0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68,
|
|
||||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d,
|
|
||||||
0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
|
||||||
0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73,
|
|
||||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
|
|
||||||
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a,
|
|
||||||
0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x05, 0x65, 0x6e,
|
|
||||||
0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x47, 0x65, 0x6f, 0x49,
|
|
||||||
0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x86, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6f,
|
|
||||||
0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f,
|
|
||||||
0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e,
|
|
||||||
0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,
|
|
||||||
0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
|
||||||
0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f,
|
|
||||||
0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
|
||||||
0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a,
|
|
||||||
0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64,
|
|
||||||
0x65, 0x22, 0x2d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74,
|
|
||||||
0x12, 0x1e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
|
||||||
0x08, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79,
|
|
||||||
0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
|
||||||
0x74, 0x6f, 0x33,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
file_v2geo_proto_rawDescOnce sync.Once
|
|
||||||
file_v2geo_proto_rawDescData = file_v2geo_proto_rawDesc
|
|
||||||
)
|
|
||||||
|
|
||||||
func file_v2geo_proto_rawDescGZIP() []byte {
|
|
||||||
file_v2geo_proto_rawDescOnce.Do(func() {
|
|
||||||
file_v2geo_proto_rawDescData = protoimpl.X.CompressGZIP(file_v2geo_proto_rawDescData)
|
|
||||||
})
|
|
||||||
return file_v2geo_proto_rawDescData
|
|
||||||
}
|
|
||||||
|
|
||||||
var file_v2geo_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
|
||||||
var file_v2geo_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
|
||||||
var file_v2geo_proto_goTypes = []interface{}{
|
|
||||||
(Domain_Type)(0), // 0: Domain.Type
|
|
||||||
(*Domain)(nil), // 1: Domain
|
|
||||||
(*CIDR)(nil), // 2: CIDR
|
|
||||||
(*GeoIP)(nil), // 3: GeoIP
|
|
||||||
(*GeoIPList)(nil), // 4: GeoIPList
|
|
||||||
(*GeoSite)(nil), // 5: MatchGeoSite
|
|
||||||
(*GeoSiteList)(nil), // 6: GeoSiteList
|
|
||||||
(*Domain_Attribute)(nil), // 7: Domain.Attribute
|
|
||||||
}
|
|
||||||
var file_v2geo_proto_depIdxs = []int32{
|
|
||||||
0, // 0: Domain.type:type_name -> Domain.Type
|
|
||||||
7, // 1: Domain.attribute:type_name -> Domain.Attribute
|
|
||||||
2, // 2: GeoIP.cidr:type_name -> CIDR
|
|
||||||
3, // 3: GeoIPList.entry:type_name -> GeoIP
|
|
||||||
1, // 4: MatchGeoSite.domain:type_name -> Domain
|
|
||||||
5, // 5: GeoSiteList.entry:type_name -> MatchGeoSite
|
|
||||||
6, // [6:6] is the sub-list for method output_type
|
|
||||||
6, // [6:6] is the sub-list for method input_type
|
|
||||||
6, // [6:6] is the sub-list for extension type_name
|
|
||||||
6, // [6:6] is the sub-list for extension extendee
|
|
||||||
0, // [0:6] is the sub-list for field type_name
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() { file_v2geo_proto_init() }
|
|
||||||
func file_v2geo_proto_init() {
|
|
||||||
if File_v2geo_proto != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !protoimpl.UnsafeEnabled {
|
|
||||||
file_v2geo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*Domain); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_v2geo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*CIDR); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_v2geo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*GeoIP); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_v2geo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*GeoIPList); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_v2geo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*GeoSite); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_v2geo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*GeoSiteList); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_v2geo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
|
|
||||||
switch v := v.(*Domain_Attribute); i {
|
|
||||||
case 0:
|
|
||||||
return &v.state
|
|
||||||
case 1:
|
|
||||||
return &v.sizeCache
|
|
||||||
case 2:
|
|
||||||
return &v.unknownFields
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_v2geo_proto_msgTypes[6].OneofWrappers = []interface{}{
|
|
||||||
(*Domain_Attribute_BoolValue)(nil),
|
|
||||||
(*Domain_Attribute_IntValue)(nil),
|
|
||||||
}
|
|
||||||
type x struct{}
|
|
||||||
out := protoimpl.TypeBuilder{
|
|
||||||
File: protoimpl.DescBuilder{
|
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
|
||||||
RawDescriptor: file_v2geo_proto_rawDesc,
|
|
||||||
NumEnums: 1,
|
|
||||||
NumMessages: 7,
|
|
||||||
NumExtensions: 0,
|
|
||||||
NumServices: 0,
|
|
||||||
},
|
|
||||||
GoTypes: file_v2geo_proto_goTypes,
|
|
||||||
DependencyIndexes: file_v2geo_proto_depIdxs,
|
|
||||||
EnumInfos: file_v2geo_proto_enumTypes,
|
|
||||||
MessageInfos: file_v2geo_proto_msgTypes,
|
|
||||||
}.Build()
|
|
||||||
File_v2geo_proto = out.File
|
|
||||||
file_v2geo_proto_rawDesc = nil
|
|
||||||
file_v2geo_proto_goTypes = nil
|
|
||||||
file_v2geo_proto_depIdxs = nil
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
option go_package = "./v2geo";
|
|
||||||
|
|
||||||
// This file is copied from
|
|
||||||
// https://github.com/v2fly/v2ray-core/blob/master/app/router/routercommon/common.proto
|
|
||||||
// with some modifications.
|
|
||||||
|
|
||||||
// Domain for routing decision.
|
|
||||||
message Domain {
|
|
||||||
// Type of domain value.
|
|
||||||
enum Type {
|
|
||||||
// The value is used as is.
|
|
||||||
Plain = 0;
|
|
||||||
// The value is used as a regular expression.
|
|
||||||
Regex = 1;
|
|
||||||
// The value is a root domain.
|
|
||||||
RootDomain = 2;
|
|
||||||
// The value is a domain.
|
|
||||||
Full = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain matching type.
|
|
||||||
Type type = 1;
|
|
||||||
|
|
||||||
// Domain value.
|
|
||||||
string value = 2;
|
|
||||||
|
|
||||||
message Attribute {
|
|
||||||
string key = 1;
|
|
||||||
|
|
||||||
oneof typed_value {
|
|
||||||
bool bool_value = 2;
|
|
||||||
int64 int_value = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attributes of this domain. May be used for filtering.
|
|
||||||
repeated Attribute attribute = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP for routing decision, in CIDR form.
|
|
||||||
message CIDR {
|
|
||||||
// IP address, should be either 4 or 16 bytes.
|
|
||||||
bytes ip = 1;
|
|
||||||
|
|
||||||
// Number of leading ones in the network mask.
|
|
||||||
uint32 prefix = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GeoIP {
|
|
||||||
string country_code = 1;
|
|
||||||
repeated CIDR cidr = 2;
|
|
||||||
bool inverse_match = 3;
|
|
||||||
|
|
||||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
|
||||||
bytes resource_hash = 4;
|
|
||||||
string code = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GeoIPList {
|
|
||||||
repeated GeoIP entry = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GeoSite {
|
|
||||||
string country_code = 1;
|
|
||||||
repeated Domain domain = 2;
|
|
||||||
|
|
||||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
|
||||||
bytes resource_hash = 3;
|
|
||||||
string code = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GeoSiteList {
|
|
||||||
repeated GeoSite entry = 1;
|
|
||||||
}
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
package ruleset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/expr-lang/expr/builtin"
|
|
||||||
|
|
||||||
"github.com/expr-lang/expr"
|
|
||||||
"github.com/expr-lang/expr/ast"
|
|
||||||
"github.com/expr-lang/expr/conf"
|
|
||||||
"github.com/expr-lang/expr/vm"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/modifier"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset/builtins"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExprRule is the external representation of an expression rule.
|
|
||||||
type ExprRule struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Action string `yaml:"action"`
|
|
||||||
Log bool `yaml:"log"`
|
|
||||||
Modifier ModifierEntry `yaml:"modifier"`
|
|
||||||
Expr string `yaml:"expr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModifierEntry struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Args map[string]interface{} `yaml:"args"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExprRulesFromYAML(file string) ([]ExprRule, error) {
|
|
||||||
bs, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var rules []ExprRule
|
|
||||||
err = yaml.Unmarshal(bs, &rules)
|
|
||||||
return rules, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// compiledExprRule is the internal, compiled representation of an expression rule.
|
|
||||||
type compiledExprRule struct {
|
|
||||||
Name string
|
|
||||||
Action *Action // fallthrough if nil
|
|
||||||
Log bool
|
|
||||||
ModInstance modifier.Instance
|
|
||||||
Program *vm.Program
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Ruleset = (*exprRuleset)(nil)
|
|
||||||
|
|
||||||
type exprRuleset struct {
|
|
||||||
Rules []compiledExprRule
|
|
||||||
Ans []analyzer.Analyzer
|
|
||||||
Logger Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *exprRuleset) Analyzers(info StreamInfo) []analyzer.Analyzer {
|
|
||||||
return r.Ans
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *exprRuleset) Match(info StreamInfo) MatchResult {
|
|
||||||
env := streamInfoToExprEnv(info)
|
|
||||||
for _, rule := range r.Rules {
|
|
||||||
v, err := vm.Run(rule.Program, env)
|
|
||||||
if err != nil {
|
|
||||||
// Log the error and continue to the next rule.
|
|
||||||
r.Logger.MatchError(info, rule.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if vBool, ok := v.(bool); ok && vBool {
|
|
||||||
if rule.Log {
|
|
||||||
r.Logger.Log(info, rule.Name)
|
|
||||||
}
|
|
||||||
if rule.Action != nil {
|
|
||||||
return MatchResult{
|
|
||||||
Action: *rule.Action,
|
|
||||||
ModInstance: rule.ModInstance,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No match
|
|
||||||
return MatchResult{
|
|
||||||
Action: ActionMaybe,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompileExprRules compiles a list of expression rules into a ruleset.
|
|
||||||
// It returns an error if any of the rules are invalid, or if any of the analyzers
|
|
||||||
// used by the rules are unknown (not provided in the analyzer list).
|
|
||||||
func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier.Modifier, config *BuiltinConfig) (Ruleset, error) {
|
|
||||||
var compiledRules []compiledExprRule
|
|
||||||
fullAnMap := analyzersToMap(ans)
|
|
||||||
fullModMap := modifiersToMap(mods)
|
|
||||||
depAnMap := make(map[string]analyzer.Analyzer)
|
|
||||||
funcMap := buildFunctionMap(config)
|
|
||||||
// Compile all rules and build a map of analyzers that are used by the rules.
|
|
||||||
for _, rule := range rules {
|
|
||||||
if rule.Action == "" && !rule.Log {
|
|
||||||
return nil, fmt.Errorf("rule %q must have at least one of action or log", rule.Name)
|
|
||||||
}
|
|
||||||
var action *Action
|
|
||||||
if rule.Action != "" {
|
|
||||||
a, ok := actionStringToAction(rule.Action)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action)
|
|
||||||
}
|
|
||||||
action = &a
|
|
||||||
}
|
|
||||||
visitor := &idVisitor{Variables: make(map[string]bool), Identifiers: make(map[string]bool)}
|
|
||||||
patcher := &idPatcher{FuncMap: funcMap}
|
|
||||||
program, err := expr.Compile(rule.Expr,
|
|
||||||
func(c *conf.Config) {
|
|
||||||
c.Strict = false
|
|
||||||
c.Expect = reflect.Bool
|
|
||||||
c.Visitors = append(c.Visitors, visitor, patcher)
|
|
||||||
for name, f := range funcMap {
|
|
||||||
c.Functions[name] = &builtin.Function{
|
|
||||||
Name: name,
|
|
||||||
Func: f.Func,
|
|
||||||
Types: f.Types,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("rule %q has invalid expression: %w", rule.Name, err)
|
|
||||||
}
|
|
||||||
if patcher.Err != nil {
|
|
||||||
return nil, fmt.Errorf("rule %q failed to patch expression: %w", rule.Name, patcher.Err)
|
|
||||||
}
|
|
||||||
for name := range visitor.Identifiers {
|
|
||||||
// Skip built-in analyzers & user-defined variables
|
|
||||||
if isBuiltInAnalyzer(name) || visitor.Variables[name] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if f, ok := funcMap[name]; ok {
|
|
||||||
// Built-in function, initialize if necessary
|
|
||||||
if f.InitFunc != nil {
|
|
||||||
if err := f.InitFunc(); err != nil {
|
|
||||||
return nil, fmt.Errorf("rule %q failed to initialize function %q: %w", rule.Name, name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if a, ok := fullAnMap[name]; ok {
|
|
||||||
// Analyzer, add to dependency map
|
|
||||||
depAnMap[name] = a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cr := compiledExprRule{
|
|
||||||
Name: rule.Name,
|
|
||||||
Action: action,
|
|
||||||
Log: rule.Log,
|
|
||||||
Program: program,
|
|
||||||
}
|
|
||||||
if action != nil && *action == ActionModify {
|
|
||||||
mod, ok := fullModMap[rule.Modifier.Name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("rule %q uses unknown modifier %q", rule.Name, rule.Modifier.Name)
|
|
||||||
}
|
|
||||||
modInst, err := mod.New(rule.Modifier.Args)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("rule %q failed to create modifier instance: %w", rule.Name, err)
|
|
||||||
}
|
|
||||||
cr.ModInstance = modInst
|
|
||||||
}
|
|
||||||
compiledRules = append(compiledRules, cr)
|
|
||||||
}
|
|
||||||
// Convert the analyzer map to a list.
|
|
||||||
var depAns []analyzer.Analyzer
|
|
||||||
for _, a := range depAnMap {
|
|
||||||
depAns = append(depAns, a)
|
|
||||||
}
|
|
||||||
return &exprRuleset{
|
|
||||||
Rules: compiledRules,
|
|
||||||
Ans: depAns,
|
|
||||||
Logger: config.Logger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamInfoToExprEnv(info StreamInfo) map[string]interface{} {
|
|
||||||
m := map[string]interface{}{
|
|
||||||
"id": info.ID,
|
|
||||||
"proto": info.Protocol.String(),
|
|
||||||
"ip": map[string]string{
|
|
||||||
"src": info.SrcIP.String(),
|
|
||||||
"dst": info.DstIP.String(),
|
|
||||||
},
|
|
||||||
"port": map[string]uint16{
|
|
||||||
"src": info.SrcPort,
|
|
||||||
"dst": info.DstPort,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for anName, anProps := range info.Props {
|
|
||||||
if len(anProps) != 0 {
|
|
||||||
// Ignore analyzers with empty properties
|
|
||||||
m[anName] = anProps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBuiltInAnalyzer(name string) bool {
|
|
||||||
switch name {
|
|
||||||
case "id", "proto", "ip", "port":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionStringToAction(action string) (Action, bool) {
|
|
||||||
switch strings.ToLower(action) {
|
|
||||||
case "allow":
|
|
||||||
return ActionAllow, true
|
|
||||||
case "block":
|
|
||||||
return ActionBlock, true
|
|
||||||
case "drop":
|
|
||||||
return ActionDrop, true
|
|
||||||
case "modify":
|
|
||||||
return ActionModify, true
|
|
||||||
default:
|
|
||||||
return ActionMaybe, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// analyzersToMap converts a list of analyzers to a map of name -> analyzer.
|
|
||||||
// This is for easier lookup when compiling rules.
|
|
||||||
func analyzersToMap(ans []analyzer.Analyzer) map[string]analyzer.Analyzer {
|
|
||||||
anMap := make(map[string]analyzer.Analyzer)
|
|
||||||
for _, a := range ans {
|
|
||||||
anMap[a.Name()] = a
|
|
||||||
}
|
|
||||||
return anMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// modifiersToMap converts a list of modifiers to a map of name -> modifier.
|
|
||||||
// This is for easier lookup when compiling rules.
|
|
||||||
func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {
|
|
||||||
modMap := make(map[string]modifier.Modifier)
|
|
||||||
for _, m := range mods {
|
|
||||||
modMap[m.Name()] = m
|
|
||||||
}
|
|
||||||
return modMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// idVisitor is a visitor that collects all identifiers in an expression.
|
|
||||||
// This is for determining which analyzers are used by the expression.
|
|
||||||
type idVisitor struct {
|
|
||||||
Variables map[string]bool
|
|
||||||
Identifiers map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *idVisitor) Visit(node *ast.Node) {
|
|
||||||
if varNode, ok := (*node).(*ast.VariableDeclaratorNode); ok {
|
|
||||||
v.Variables[varNode.Name] = true
|
|
||||||
} else if idNode, ok := (*node).(*ast.IdentifierNode); ok {
|
|
||||||
v.Identifiers[idNode.Value] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// idPatcher patches the AST during expr compilation, replacing certain values with
|
|
||||||
// their internal representations for better runtime performance.
|
|
||||||
type idPatcher struct {
|
|
||||||
FuncMap map[string]*Function
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *idPatcher) Visit(node *ast.Node) {
|
|
||||||
switch (*node).(type) {
|
|
||||||
case *ast.CallNode:
|
|
||||||
callNode := (*node).(*ast.CallNode)
|
|
||||||
if callNode.Callee == nil {
|
|
||||||
// Ignore invalid call nodes
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if f, ok := p.FuncMap[callNode.Callee.String()]; ok {
|
|
||||||
if f.PatchFunc != nil {
|
|
||||||
if err := f.PatchFunc(&callNode.Arguments); err != nil {
|
|
||||||
p.Err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Function struct {
|
|
||||||
InitFunc func() error
|
|
||||||
PatchFunc func(args *[]ast.Node) error
|
|
||||||
Func func(params ...any) (any, error)
|
|
||||||
Types []reflect.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFunctionMap(config *BuiltinConfig) map[string]*Function {
|
|
||||||
return map[string]*Function{
|
|
||||||
"geoip": {
|
|
||||||
InitFunc: config.GeoMatcher.LoadGeoIP,
|
|
||||||
PatchFunc: nil,
|
|
||||||
Func: func(params ...any) (any, error) {
|
|
||||||
return config.GeoMatcher.MatchGeoIp(params[0].(string), params[1].(string)), nil
|
|
||||||
},
|
|
||||||
Types: []reflect.Type{reflect.TypeOf(config.GeoMatcher.MatchGeoIp)},
|
|
||||||
},
|
|
||||||
"geosite": {
|
|
||||||
InitFunc: config.GeoMatcher.LoadGeoSite,
|
|
||||||
PatchFunc: nil,
|
|
||||||
Func: func(params ...any) (any, error) {
|
|
||||||
return config.GeoMatcher.MatchGeoSite(params[0].(string), params[1].(string)), nil
|
|
||||||
},
|
|
||||||
Types: []reflect.Type{reflect.TypeOf(config.GeoMatcher.MatchGeoSite)},
|
|
||||||
},
|
|
||||||
"cidr": {
|
|
||||||
InitFunc: nil,
|
|
||||||
PatchFunc: func(args *[]ast.Node) error {
|
|
||||||
cidrStringNode, ok := (*args)[1].(*ast.StringNode)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("cidr: invalid argument type")
|
|
||||||
}
|
|
||||||
cidr, err := builtins.CompileCIDR(cidrStringNode.Value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
(*args)[1] = &ast.ConstantNode{Value: cidr}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Func: func(params ...any) (any, error) {
|
|
||||||
return builtins.MatchCIDR(params[0].(string), params[1].(*net.IPNet)), nil
|
|
||||||
},
|
|
||||||
Types: []reflect.Type{reflect.TypeOf(builtins.MatchCIDR)},
|
|
||||||
},
|
|
||||||
"lookup": {
|
|
||||||
InitFunc: nil,
|
|
||||||
PatchFunc: func(args *[]ast.Node) error {
|
|
||||||
var serverStr *ast.StringNode
|
|
||||||
if len(*args) > 1 {
|
|
||||||
// Has the optional server argument
|
|
||||||
var ok bool
|
|
||||||
serverStr, ok = (*args)[1].(*ast.StringNode)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("lookup: invalid argument type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r := &net.Resolver{
|
|
||||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
if serverStr != nil {
|
|
||||||
address = serverStr.Value
|
|
||||||
}
|
|
||||||
return config.ProtectedDialContext(ctx, network, address)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if len(*args) > 1 {
|
|
||||||
(*args)[1] = &ast.ConstantNode{Value: r}
|
|
||||||
} else {
|
|
||||||
*args = append(*args, &ast.ConstantNode{Value: r})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Func: func(params ...any) (any, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
return params[1].(*net.Resolver).LookupHost(ctx, params[0].(string))
|
|
||||||
},
|
|
||||||
Types: []reflect.Type{
|
|
||||||
reflect.TypeOf((func(string, *net.Resolver) []string)(nil)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package ruleset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/apernet/OpenGFW/analyzer"
|
|
||||||
"github.com/apernet/OpenGFW/modifier"
|
|
||||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Action int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ActionMaybe indicates that the ruleset hasn't seen anything worth blocking based on
|
|
||||||
// current information, but that may change if volatile fields change in the future.
|
|
||||||
ActionMaybe Action = iota
|
|
||||||
// ActionAllow indicates that the stream should be allowed regardless of future changes.
|
|
||||||
ActionAllow
|
|
||||||
// ActionBlock indicates that the stream should be blocked.
|
|
||||||
ActionBlock
|
|
||||||
// ActionDrop indicates that the current packet should be dropped,
|
|
||||||
// but the stream should be allowed to continue.
|
|
||||||
// Only valid for UDP streams. Equivalent to ActionBlock for TCP streams.
|
|
||||||
ActionDrop
|
|
||||||
// ActionModify indicates that the current packet should be modified,
|
|
||||||
// and the stream should be allowed to continue.
|
|
||||||
// Only valid for UDP streams. Equivalent to ActionMaybe for TCP streams.
|
|
||||||
ActionModify
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a Action) String() string {
|
|
||||||
switch a {
|
|
||||||
case ActionMaybe:
|
|
||||||
return "maybe"
|
|
||||||
case ActionAllow:
|
|
||||||
return "allow"
|
|
||||||
case ActionBlock:
|
|
||||||
return "block"
|
|
||||||
case ActionDrop:
|
|
||||||
return "drop"
|
|
||||||
case ActionModify:
|
|
||||||
return "modify"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Protocol int
|
|
||||||
|
|
||||||
func (p Protocol) String() string {
|
|
||||||
switch p {
|
|
||||||
case ProtocolTCP:
|
|
||||||
return "tcp"
|
|
||||||
case ProtocolUDP:
|
|
||||||
return "udp"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProtocolTCP Protocol = iota
|
|
||||||
ProtocolUDP
|
|
||||||
)
|
|
||||||
|
|
||||||
type StreamInfo struct {
|
|
||||||
ID int64
|
|
||||||
Protocol Protocol
|
|
||||||
SrcIP, DstIP net.IP
|
|
||||||
SrcPort, DstPort uint16
|
|
||||||
Props analyzer.CombinedPropMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i StreamInfo) SrcString() string {
|
|
||||||
return net.JoinHostPort(i.SrcIP.String(), strconv.Itoa(int(i.SrcPort)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i StreamInfo) DstString() string {
|
|
||||||
return net.JoinHostPort(i.DstIP.String(), strconv.Itoa(int(i.DstPort)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type MatchResult struct {
|
|
||||||
Action Action
|
|
||||||
ModInstance modifier.Instance
|
|
||||||
}
|
|
||||||
|
|
||||||
type Ruleset interface {
|
|
||||||
// Analyzers returns the list of analyzers to use for a stream.
|
|
||||||
// It must be safe for concurrent use by multiple workers.
|
|
||||||
Analyzers(StreamInfo) []analyzer.Analyzer
|
|
||||||
// Match matches a stream against the ruleset and returns the result.
|
|
||||||
// It must be safe for concurrent use by multiple workers.
|
|
||||||
Match(StreamInfo) MatchResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger is the logging interface for the ruleset.
|
|
||||||
type Logger interface {
|
|
||||||
Log(info StreamInfo, name string)
|
|
||||||
MatchError(info StreamInfo, name string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BuiltinConfig struct {
|
|
||||||
Logger Logger
|
|
||||||
GeoMatcher *geo.GeoMatcher
|
|
||||||
ProtectedDialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
|
||||||
}
|
|
||||||
Executable
BIN
Binary file not shown.
@@ -115,22 +115,6 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if runtime.GOOS == "linux" {
|
|
||||||
// go func() {
|
|
||||||
|
|
||||||
// ruleFile := "./rules.yaml"
|
|
||||||
// if _, err := os.Stat(ruleFile); os.IsNotExist(err) {
|
|
||||||
// if err := os.WriteFile(ruleFile, []byte(""), 0644); err != nil {
|
|
||||||
// log.Printf("failed to create rules file: %v", err)
|
|
||||||
// } else {
|
|
||||||
// log.Printf("created empty rules file: %s", ruleFile)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// cmd.Execute([]string{ruleFile})
|
|
||||||
// }()
|
|
||||||
// }
|
|
||||||
|
|
||||||
fmt.Println("✅ 配置加载成功 - addr: %s", config.Addr)
|
fmt.Println("✅ 配置加载成功 - addr: %s", config.Addr)
|
||||||
|
|
||||||
log := xlogger.NewLogger()
|
log := xlogger.NewLogger()
|
||||||
@@ -147,5 +131,4 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
// GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
||||||
//
|
|
||||||
// upx --best --lzma gost
|
// upx --best --lzma gost
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -40,8 +44,25 @@ type options struct {
|
|||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isTls = 0
|
||||||
|
|
||||||
|
var isHttp = 0
|
||||||
|
|
||||||
|
var isSocks = 0
|
||||||
|
|
||||||
|
var needWrap = false
|
||||||
|
|
||||||
type Option func(opts *options)
|
type Option func(opts *options)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_, err := LoadConfig("config.json")
|
||||||
|
fmt.Println("config.json loaded")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
needWrap = isTls+isSocks+isHttp > 0
|
||||||
|
}
|
||||||
|
|
||||||
func AdmissionOption(admission admission.Admission) Option {
|
func AdmissionOption(admission admission.Admission) Option {
|
||||||
return func(opts *options) {
|
return func(opts *options) {
|
||||||
opts.admission = admission
|
opts.admission = admission
|
||||||
@@ -138,6 +159,7 @@ func (s *defaultService) Addr() net.Addr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *defaultService) Serve() error {
|
func (s *defaultService) Serve() error {
|
||||||
|
|
||||||
s.execCmds("post-up", s.options.postUp)
|
s.execCmds("post-up", s.options.postUp)
|
||||||
s.setState(StateReady)
|
s.setState(StateReady)
|
||||||
s.status.addEvent(Event{
|
s.status.addEvent(Event{
|
||||||
@@ -166,6 +188,7 @@ func (s *defaultService) Serve() error {
|
|||||||
for {
|
for {
|
||||||
conn, e := s.listener.Accept()
|
conn, e := s.listener.Accept()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
|
||||||
// TODO: remove Temporary checking
|
// TODO: remove Temporary checking
|
||||||
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
||||||
if tempDelay == 0 {
|
if tempDelay == 0 {
|
||||||
@@ -256,6 +279,10 @@ func (s *defaultService) Serve() error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if needWrap {
|
||||||
|
conn = wrapConnPDetection(conn)
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.handler.Handle(ctx, conn); err != nil {
|
if err := s.handler.Handle(ctx, conn); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
if v := xmetrics.GetCounter(xmetrics.MetricServiceHandlerErrorsCounter,
|
if v := xmetrics.GetCounter(xmetrics.MetricServiceHandlerErrorsCounter,
|
||||||
@@ -406,3 +433,150 @@ type ServiceEvent struct {
|
|||||||
func (ServiceEvent) Type() observer.EventType {
|
func (ServiceEvent) Type() observer.EventType {
|
||||||
return observer.EventStatus
|
return observer.EventStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrapConnPDetection(conn net.Conn) net.Conn {
|
||||||
|
return &detectConn{
|
||||||
|
Conn: conn,
|
||||||
|
reader: bufio.NewReader(conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type detectConn struct {
|
||||||
|
net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
detected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *detectConn) Read(b []byte) (int, error) {
|
||||||
|
n, err := c.reader.Read(b)
|
||||||
|
if n > 0 && !c.detected {
|
||||||
|
c.detected = true
|
||||||
|
if detectProtocol(b[:n], c.Conn) {
|
||||||
|
return 0, fmt.Errorf("connection blocked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectProtocol(data []byte, conn net.Conn) (blocked bool) {
|
||||||
|
if isHttp == 1 && detectHTTP(data) {
|
||||||
|
conn.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTls == 1 && detectTLS(data) {
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSocks == 1 && detectSOCKS(data) {
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectHTTP(data []byte) bool {
|
||||||
|
if len(data) < 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(data) >= 3 && data[0] == 'G' && data[1] == 'E' && data[2] == 'T':
|
||||||
|
return true
|
||||||
|
case len(data) >= 4 && data[0] == 'P' && data[1] == 'O' && data[2] == 'S' && data[3] == 'T':
|
||||||
|
return true
|
||||||
|
case len(data) >= 3 && data[0] == 'P' && data[1] == 'U' && data[2] == 'T':
|
||||||
|
return true
|
||||||
|
case len(data) >= 6 && data[0] == 'D' && data[1] == 'E' && data[2] == 'L' &&
|
||||||
|
data[3] == 'E' && data[4] == 'T' && data[5] == 'E':
|
||||||
|
return true
|
||||||
|
case len(data) >= 4 && data[0] == 'H' && data[1] == 'E' && data[2] == 'A' && data[3] == 'D':
|
||||||
|
return true
|
||||||
|
case len(data) >= 7 && data[0] == 'O' && data[1] == 'P' && data[2] == 'T' &&
|
||||||
|
data[3] == 'I' && data[4] == 'O' && data[5] == 'N' && data[6] == 'S':
|
||||||
|
return true
|
||||||
|
case len(data) >= 5 && data[0] == 'P' && data[1] == 'A' && data[2] == 'T' &&
|
||||||
|
data[3] == 'C' && data[4] == 'H':
|
||||||
|
return true
|
||||||
|
case len(data) >= 7 && data[0] == 'C' && data[1] == 'O' && data[2] == 'N' &&
|
||||||
|
data[3] == 'N' && data[4] == 'E' && data[5] == 'C' && data[6] == 'T': // HTTPS proxy
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectTLS(data []byte) bool {
|
||||||
|
if len(data) < 5 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if data[0] == 0x16 && data[1] == 0x03 && data[2] >= 0x01 && data[2] <= 0x04 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectSOCKS(data []byte) bool {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch data[0] {
|
||||||
|
case 0x04:
|
||||||
|
if len(data) < 7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cmd := data[1]
|
||||||
|
if cmd != 0x01 && cmd != 0x02 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case 0x05:
|
||||||
|
if len(data) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nMethods := int(data[1])
|
||||||
|
if len(data) < 2+nMethods {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, method := range data[2 : 2+nMethods] {
|
||||||
|
if method == 0x00 || method == 0x02 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 配置结构体
|
||||||
|
type Config struct {
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Http int `json:"http"`
|
||||||
|
Tls int `json:"tls"`
|
||||||
|
Socks int `json:"socks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(configPath string) (string, error) {
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("配置文件不存在: %s", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return "", fmt.Errorf("解析配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isTls = config.Tls
|
||||||
|
isSocks = config.Socks
|
||||||
|
isHttp = config.Http
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
+2
-234
@@ -12,8 +12,7 @@ show_menu() {
|
|||||||
echo "1. 安装"
|
echo "1. 安装"
|
||||||
echo "2. 更新"
|
echo "2. 更新"
|
||||||
echo "3. 卸载"
|
echo "3. 卸载"
|
||||||
# echo "4. 屏蔽协议"
|
echo "4. 退出"
|
||||||
echo "5. 退出"
|
|
||||||
echo "==============================================="
|
echo "==============================================="
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,232 +93,6 @@ check_and_install_tcpkill() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# 检查并安装 iptables
|
|
||||||
check_and_install_iptables() {
|
|
||||||
echo "🔍 检查 iptables..."
|
|
||||||
|
|
||||||
# 检查 iptables 是否已安装
|
|
||||||
if command -v iptables &> /dev/null; then
|
|
||||||
echo "✅ iptables 已安装"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📦 iptables 未安装,正在安装..."
|
|
||||||
|
|
||||||
# 检查是否需要 sudo
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
SUDO_CMD="sudo"
|
|
||||||
else
|
|
||||||
SUDO_CMD=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检测操作系统类型
|
|
||||||
OS_TYPE=$(uname -s)
|
|
||||||
|
|
||||||
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
|
||||||
echo "⚠️ macOS 系统不支持 iptables,请手动安装或使用 pfctl"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检测 Linux 发行版并安装对应的包
|
|
||||||
if [ -f /etc/os-release ]; then
|
|
||||||
. /etc/os-release
|
|
||||||
DISTRO=$ID
|
|
||||||
elif [ -f /etc/redhat-release ]; then
|
|
||||||
DISTRO="rhel"
|
|
||||||
elif [ -f /etc/debian_version ]; then
|
|
||||||
DISTRO="debian"
|
|
||||||
else
|
|
||||||
echo "❌ 无法识别的操作系统"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $DISTRO in
|
|
||||||
ubuntu|debian)
|
|
||||||
$SUDO_CMD apt update &> /dev/null
|
|
||||||
$SUDO_CMD apt install -y iptables &> /dev/null
|
|
||||||
;;
|
|
||||||
centos|rhel|fedora)
|
|
||||||
if command -v dnf &> /dev/null; then
|
|
||||||
$SUDO_CMD dnf install -y iptables &> /dev/null
|
|
||||||
elif command -v yum &> /dev/null; then
|
|
||||||
$SUDO_CMD yum install -y iptables &> /dev/null
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
alpine)
|
|
||||||
$SUDO_CMD apk add --no-cache iptables &> /dev/null
|
|
||||||
;;
|
|
||||||
arch|manjaro)
|
|
||||||
$SUDO_CMD pacman -S --noconfirm iptables &> /dev/null
|
|
||||||
;;
|
|
||||||
opensuse*|sles)
|
|
||||||
$SUDO_CMD zypper install -y iptables &> /dev/null
|
|
||||||
;;
|
|
||||||
gentoo)
|
|
||||||
$SUDO_CMD emerge --ask=n net-firewall/iptables &> /dev/null
|
|
||||||
;;
|
|
||||||
void)
|
|
||||||
$SUDO_CMD xbps-install -Sy iptables &> /dev/null
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# 验证安装
|
|
||||||
if command -v iptables &> /dev/null; then
|
|
||||||
echo "✅ iptables 安装成功"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "❌ iptables 安装失败"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 屏蔽协议功能
|
|
||||||
block_protocol() {
|
|
||||||
echo "🛡️ 屏蔽协议功能"
|
|
||||||
echo "==============================================="
|
|
||||||
|
|
||||||
# 检查 GOST 是否已安装
|
|
||||||
if [[ ! -d "$INSTALL_DIR" || ! -f "$INSTALL_DIR/gost" ]]; then
|
|
||||||
echo "❌ GOST 服务未安装,请先选择安装选项"
|
|
||||||
echo "💡 提示:请先运行选项 1 安装 GOST 服务"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查 GOST 服务是否正在运行
|
|
||||||
if ! systemctl is-active --quiet gost; then
|
|
||||||
echo "⚠️ GOST 服务未运行,正在启动..."
|
|
||||||
systemctl start gost
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
if ! systemctl is-active --quiet gost; then
|
|
||||||
echo "❌ GOST 服务启动失败,请检查配置"
|
|
||||||
echo "💡 提示:请运行 'journalctl -u gost -f' 查看详细错误信息"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ GOST 服务检测通过"
|
|
||||||
|
|
||||||
检查并安装 iptables
|
|
||||||
if ! check_and_install_iptables; then
|
|
||||||
echo "❌ iptables 检查失败,无法继续"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 验证 IPv4
|
|
||||||
is_ipv4() {
|
|
||||||
[[ $1 =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 验证 IPv6(简单正则)
|
|
||||||
is_ipv6() {
|
|
||||||
[[ $1 =~ ^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$ ]] && return 0 || return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ips=() # 用于存储所有输入的 IP
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
read -p "请输入 IP(多个用逗号分隔,输入 n 结束): " input
|
|
||||||
# 判断是否输入 n
|
|
||||||
if [[ "$input" == "n" ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 使用逗号分割
|
|
||||||
IFS=',' read -ra arr <<< "$input"
|
|
||||||
for ip in "${arr[@]}"; do
|
|
||||||
ip_trimmed=$(echo "$ip" | xargs) # 去掉首尾空格
|
|
||||||
if [[ -z "$ip_trimmed" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
if is_ipv4 "$ip_trimmed" || is_ipv6 "$ip_trimmed"; then
|
|
||||||
ips+=("$ip_trimmed")
|
|
||||||
else
|
|
||||||
echo "⚠️ 无效 IP: $ip_trimmed"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
# 打印记录的 IP
|
|
||||||
for ip in "${ips[@]}"; do
|
|
||||||
echo -e "\e[32m$ip\e[0m"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
read -p "是否屏蔽 HTTP? (y/n) [n]: " block_http
|
|
||||||
block_http=${block_http:-n}
|
|
||||||
|
|
||||||
read -p "是否屏蔽 TLS? (y/n) [n]: " block_tls
|
|
||||||
block_tls=${block_tls:-n}
|
|
||||||
|
|
||||||
read -p "是否屏蔽 SOCKS5? (y/n) [n]: " block_socks5
|
|
||||||
block_socks5=${block_socks5:-n}
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🛡️ 屏蔽设置结果:"
|
|
||||||
[[ "$block_http" == "y" ]] && echo " - HTTP 已屏蔽" || echo " - HTTP 未屏蔽"
|
|
||||||
[[ "$block_tls" == "y" ]] && echo " - TLS 已屏蔽" || echo " - TLS 未屏蔽"
|
|
||||||
[[ "$block_socks5" == "y" ]] && echo " - SOCKS5 已屏蔽" || echo " - SOCKS5 未屏蔽"
|
|
||||||
|
|
||||||
# 生成 rules.yaml 文件
|
|
||||||
local file="/etc/gost/rules.yaml"
|
|
||||||
> "$file"
|
|
||||||
|
|
||||||
# 构造 IP 排除字符串
|
|
||||||
ip_expr=""
|
|
||||||
for ip in "${ips[@]}"; do
|
|
||||||
ip_expr+="ip.src != \"$ip\" && "
|
|
||||||
done
|
|
||||||
ip_expr=${ip_expr% && }
|
|
||||||
|
|
||||||
# 写入规则
|
|
||||||
[[ "$block_http" == "y" ]] && cat >> "$file" <<EOF
|
|
||||||
- name: block http
|
|
||||||
action: block
|
|
||||||
log: true
|
|
||||||
expr: http != nil && $ip_expr
|
|
||||||
EOF
|
|
||||||
|
|
||||||
[[ "$block_tls" == "y" ]] && cat >> "$file" <<EOF
|
|
||||||
|
|
||||||
- name: block tls
|
|
||||||
action: block
|
|
||||||
log: true
|
|
||||||
expr: tls != nil && $ip_expr
|
|
||||||
EOF
|
|
||||||
|
|
||||||
[[ "$block_socks5" == "y" ]] && cat >> "$file" <<EOF
|
|
||||||
|
|
||||||
- name: block socks
|
|
||||||
action: block
|
|
||||||
log: true
|
|
||||||
expr: socks != nil && $ip_expr
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "📝 已生成 $file"
|
|
||||||
|
|
||||||
# 重启 GOST 服务
|
|
||||||
echo ""
|
|
||||||
echo "🔄 重启 GOST 服务..."
|
|
||||||
systemctl restart gost
|
|
||||||
echo "5s后检查服务状态"
|
|
||||||
sleep 5
|
|
||||||
# 检查状态
|
|
||||||
echo "检查服务状态..."
|
|
||||||
if systemctl is-active --quiet gost; then
|
|
||||||
echo "✅ 配置完成,gost服务已重启并正常运行。"
|
|
||||||
echo "📁 配置目录: $INSTALL_DIR"
|
|
||||||
echo "🔧 服务状态: $(systemctl is-active gost)"
|
|
||||||
else
|
|
||||||
echo "❌ gost服务启动失败,请执行以下命令查看日志:"
|
|
||||||
echo "journalctl -u gost -f"
|
|
||||||
fi
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
# 获取用户输入的配置参数
|
# 获取用户输入的配置参数
|
||||||
get_config_params() {
|
get_config_params() {
|
||||||
@@ -370,9 +143,7 @@ install_gost() {
|
|||||||
# 检查并安装 tcpkill
|
# 检查并安装 tcpkill
|
||||||
check_and_install_tcpkill
|
check_and_install_tcpkill
|
||||||
|
|
||||||
# 检查并安装 iptables
|
|
||||||
check_and_install_iptables
|
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
# 停止并禁用已有服务
|
# 停止并禁用已有服务
|
||||||
@@ -478,9 +249,6 @@ update_gost() {
|
|||||||
# 检查并安装 tcpkill
|
# 检查并安装 tcpkill
|
||||||
check_and_install_tcpkill
|
check_and_install_tcpkill
|
||||||
|
|
||||||
# 检查并安装 iptables
|
|
||||||
check_and_install_iptables
|
|
||||||
|
|
||||||
# 先下载新版本
|
# 先下载新版本
|
||||||
echo "⬇️ 下载最新版本..."
|
echo "⬇️ 下载最新版本..."
|
||||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost.new"
|
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost.new"
|
||||||
|
|||||||
+123
-118
@@ -5,6 +5,7 @@ import com.admin.common.lang.R;
|
|||||||
import com.admin.common.utils.GostUtil;
|
import com.admin.common.utils.GostUtil;
|
||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
import com.admin.service.*;
|
import com.admin.service.*;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -42,11 +43,14 @@ public class CheckGostConfigAsync {
|
|||||||
*/
|
*/
|
||||||
@Async
|
@Async
|
||||||
public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) {
|
public void cleanNodeConfigs(String node_id, GostConfigDto gostConfig) {
|
||||||
|
System.out.println(JSONObject.toJSONString(gostConfig));
|
||||||
Node node = nodeService.getById(node_id);
|
Node node = nodeService.getById(node_id);
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
cleanOrphanedServices(gostConfig, node);
|
cleanOrphanedServices(gostConfig, node);
|
||||||
cleanOrphanedChains(gostConfig, node);
|
cleanOrphanedChains(gostConfig, node);
|
||||||
cleanOrphanedLimiters(gostConfig, node);
|
cleanOrphanedLimiters(gostConfig, node);
|
||||||
|
syncLimiters(gostConfig, node);
|
||||||
|
syncServices(gostConfig, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,135 +58,45 @@ public class CheckGostConfigAsync {
|
|||||||
* 清理孤立的服务
|
* 清理孤立的服务
|
||||||
*/
|
*/
|
||||||
private void cleanOrphanedServices(GostConfigDto gostConfig, Node node) {
|
private void cleanOrphanedServices(GostConfigDto gostConfig, Node node) {
|
||||||
if (gostConfig.getServices() != null) {
|
if (gostConfig.getServices() == null) {
|
||||||
for (ConfigItem service : gostConfig.getServices()) {
|
return;
|
||||||
safeExecute(() -> {
|
|
||||||
if (Objects.equals(service.getName(), "web_api")) {
|
|
||||||
return; // 排除API服务
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] serviceIds = parseServiceName(service.getName());
|
|
||||||
if (serviceIds.length == 4) {
|
|
||||||
String forwardId = serviceIds[0];
|
|
||||||
String userId = serviceIds[1];
|
|
||||||
String userTunnelId = serviceIds[2];
|
|
||||||
String type = serviceIds[3];
|
|
||||||
|
|
||||||
if (Objects.equals(type, "tcp")) { // 只处理TCP,避免重复处理
|
|
||||||
Forward forward = forwardService.getById(forwardId);
|
|
||||||
if (forward == null) {
|
|
||||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
|
||||||
GostDto gostDto = GostUtil.DeleteService(node.getId(), forwardId + "_" + userId + "_" + userTunnelId);
|
|
||||||
System.out.println(gostDto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (Objects.equals(type, "tls")) {
|
|
||||||
Forward forward = forwardService.getById(forwardId);
|
|
||||||
if (forward == null) {
|
|
||||||
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
|
||||||
GostUtil.DeleteRemoteService(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}, "清理服务 " + service.getName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (ConfigItem service : gostConfig.getServices()) {
|
||||||
|
safeExecute(() -> {
|
||||||
List<Tunnel> tunnelList = tunnelService.list(new QueryWrapper<Tunnel>().eq("in_node_id", node.getId()));
|
if (Objects.equals(service.getName(), "web_api")) {
|
||||||
|
return; // 排除API服务
|
||||||
try {
|
|
||||||
if (tunnelList != null && !tunnelList.isEmpty()) {
|
|
||||||
StringBuilder tunnelIds = new StringBuilder();
|
|
||||||
for (Tunnel tunnel : tunnelList) {
|
|
||||||
tunnelIds.append(tunnel.getId()).append(",");
|
|
||||||
}
|
}
|
||||||
String ids = tunnelIds.deleteCharAt(tunnelIds.length() - 1).toString();
|
|
||||||
List<SpeedLimit> speedLimits = speedLimitService.list(new QueryWrapper<SpeedLimit>().in("tunnel_id", ids));
|
String[] serviceIds = parseServiceName(service.getName());
|
||||||
if (speedLimits != null && !speedLimits.isEmpty()) {
|
if (serviceIds.length == 4) {
|
||||||
List<ConfigItem> limiters = gostConfig.getLimiters();
|
String forwardId = serviceIds[0];
|
||||||
List<Long> limiters_ids = new ArrayList<>();
|
String userId = serviceIds[1];
|
||||||
List<Long> speedLimits_ids = new ArrayList<>();
|
String userTunnelId = serviceIds[2];
|
||||||
if (limiters != null){
|
String type = serviceIds[3];
|
||||||
for (ConfigItem limiter : limiters) {
|
|
||||||
limiters_ids.add(Long.valueOf(limiter.getName()));
|
if (Objects.equals(type, "tcp")) { // 只处理TCP,避免重复处理
|
||||||
|
Forward forward = forwardService.getById(forwardId);
|
||||||
|
if (forward == null) {
|
||||||
|
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||||
|
GostDto gostDto = GostUtil.DeleteService(node.getId(), forwardId + "_" + userId + "_" + userTunnelId);
|
||||||
|
System.out.println(gostDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (SpeedLimit speedLimit : speedLimits) {
|
|
||||||
speedLimits_ids.add(speedLimit.getId());
|
|
||||||
}
|
|
||||||
List<Long> diff = new ArrayList<>(speedLimits_ids);
|
|
||||||
diff.removeAll(limiters_ids);
|
|
||||||
System.out.println(diff);
|
|
||||||
if (!diff.isEmpty()) {
|
|
||||||
|
|
||||||
for (Long speed_id : diff) {
|
|
||||||
SpeedLimit speedLimit = speedLimitService.getById(speed_id);
|
if (Objects.equals(type, "tls")) {
|
||||||
if (speedLimit != null) {
|
Forward forward = forwardService.getById(forwardId);
|
||||||
SpeedLimitUpdateDto speedLimitUpdateDto = new SpeedLimitUpdateDto();
|
if (forward == null) {
|
||||||
speedLimitUpdateDto.setId(speed_id);
|
log.info("删除孤立的服务: {} (节点: {})", service.getName(), node.getId());
|
||||||
speedLimitUpdateDto.setName(speedLimit.getName());
|
GostUtil.DeleteRemoteService(node.getId(), forwardId+"_"+userId+"_"+userTunnelId);
|
||||||
speedLimitUpdateDto.setSpeed(speedLimit.getSpeed());
|
|
||||||
speedLimitUpdateDto.setTunnelId(speedLimit.getTunnelId());
|
|
||||||
speedLimitUpdateDto.setTunnelName(speedLimit.getTunnelName());
|
|
||||||
speedLimitService.updateSpeedLimit(speedLimitUpdateDto);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}, "清理服务 " + service.getName());
|
||||||
}catch (Exception e){
|
|
||||||
log.info("同步限速器" + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (tunnelList != null && !tunnelList.isEmpty()) {
|
|
||||||
StringBuilder tunnelIds = new StringBuilder();
|
|
||||||
for (Tunnel tunnel : tunnelList) {
|
|
||||||
tunnelIds.append(tunnel.getId()).append(",");
|
|
||||||
}
|
|
||||||
String ids = tunnelIds.deleteCharAt(tunnelIds.length() - 1).toString();
|
|
||||||
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().in("tunnel_id", ids));
|
|
||||||
if (forwardList != null && !forwardList.isEmpty()) {
|
|
||||||
List<ConfigItem> services = gostConfig.getServices();
|
|
||||||
List<Long> services_ids = new ArrayList<>();
|
|
||||||
List<Long> forward_ids = new ArrayList<>();
|
|
||||||
if (services != null){
|
|
||||||
for (ConfigItem limiter : services) {
|
|
||||||
String[] strings = parseServiceName(limiter.getName());
|
|
||||||
services_ids.add(Long.valueOf(strings[0]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Forward forward : forwardList) {
|
|
||||||
forward_ids.add(forward.getId());
|
|
||||||
}
|
|
||||||
List<Long> diff = new ArrayList<>(forward_ids);
|
|
||||||
diff.removeAll(services_ids);
|
|
||||||
System.out.println(diff);
|
|
||||||
if (!diff.isEmpty()) {
|
|
||||||
for (Long forward_id : diff) {
|
|
||||||
Forward forward = forwardService.getById(forward_id);
|
|
||||||
if (forward != null) {
|
|
||||||
forwardService.updateForwardA(forward);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}catch (Exception e){
|
|
||||||
log.info("同步转发" + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,6 +149,96 @@ public class CheckGostConfigAsync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步限流器
|
||||||
|
*/
|
||||||
|
private void syncLimiters(GostConfigDto gostConfig, Node node) {
|
||||||
|
List<Tunnel> tunnelList = tunnelService.list(new QueryWrapper<Tunnel>().eq("in_node_id", node.getId()));
|
||||||
|
if (tunnelList == null || tunnelList.isEmpty()) return;
|
||||||
|
safeExecute(() -> {
|
||||||
|
StringBuilder tunnelIds = new StringBuilder();
|
||||||
|
for (Tunnel tunnel : tunnelList) {
|
||||||
|
tunnelIds.append(tunnel.getId()).append(",");
|
||||||
|
}
|
||||||
|
String ids = tunnelIds.deleteCharAt(tunnelIds.length() - 1).toString();
|
||||||
|
List<SpeedLimit> speedLimits = speedLimitService.list(new QueryWrapper<SpeedLimit>().in("tunnel_id", ids));
|
||||||
|
if (speedLimits != null && !speedLimits.isEmpty()) {
|
||||||
|
List<ConfigItem> limiters = gostConfig.getLimiters();
|
||||||
|
List<Long> limiters_ids = new ArrayList<>();
|
||||||
|
List<Long> speedLimits_ids = new ArrayList<>();
|
||||||
|
if (limiters != null){
|
||||||
|
for (ConfigItem limiter : limiters) {
|
||||||
|
limiters_ids.add(Long.valueOf(limiter.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SpeedLimit speedLimit : speedLimits) {
|
||||||
|
speedLimits_ids.add(speedLimit.getId());
|
||||||
|
}
|
||||||
|
List<Long> diff = new ArrayList<>(speedLimits_ids);
|
||||||
|
diff.removeAll(limiters_ids);
|
||||||
|
System.out.println(diff);
|
||||||
|
if (!diff.isEmpty()) {
|
||||||
|
|
||||||
|
for (Long speed_id : diff) {
|
||||||
|
SpeedLimit speedLimit = speedLimitService.getById(speed_id);
|
||||||
|
if (speedLimit != null) {
|
||||||
|
SpeedLimitUpdateDto speedLimitUpdateDto = new SpeedLimitUpdateDto();
|
||||||
|
speedLimitUpdateDto.setId(speed_id);
|
||||||
|
speedLimitUpdateDto.setName(speedLimit.getName());
|
||||||
|
speedLimitUpdateDto.setSpeed(speedLimit.getSpeed());
|
||||||
|
speedLimitUpdateDto.setTunnelId(speedLimit.getTunnelId());
|
||||||
|
speedLimitUpdateDto.setTunnelName(speedLimit.getTunnelName());
|
||||||
|
speedLimitService.updateSpeedLimit(speedLimitUpdateDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "同步限流器 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步服务
|
||||||
|
*/
|
||||||
|
private void syncServices(GostConfigDto gostConfig, Node node) {
|
||||||
|
List<Tunnel> tunnelList = tunnelService.list(new QueryWrapper<Tunnel>().eq("in_node_id", node.getId()));
|
||||||
|
if (tunnelList == null || tunnelList.isEmpty()) return;
|
||||||
|
safeExecute(() -> {
|
||||||
|
StringBuilder tunnelIds = new StringBuilder();
|
||||||
|
for (Tunnel tunnel : tunnelList) {
|
||||||
|
tunnelIds.append(tunnel.getId()).append(",");
|
||||||
|
}
|
||||||
|
String ids = tunnelIds.deleteCharAt(tunnelIds.length() - 1).toString();
|
||||||
|
List<Forward> forwardList = forwardService.list(new QueryWrapper<Forward>().in("tunnel_id", ids));
|
||||||
|
if (forwardList != null && !forwardList.isEmpty()) {
|
||||||
|
List<ConfigItem> services = gostConfig.getServices();
|
||||||
|
List<Long> services_ids = new ArrayList<>();
|
||||||
|
List<Long> forward_ids = new ArrayList<>();
|
||||||
|
if (services != null){
|
||||||
|
for (ConfigItem limiter : services) {
|
||||||
|
String[] strings = parseServiceName(limiter.getName());
|
||||||
|
services_ids.add(Long.valueOf(strings[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Forward forward : forwardList) {
|
||||||
|
forward_ids.add(forward.getId());
|
||||||
|
}
|
||||||
|
List<Long> diff = new ArrayList<>(forward_ids);
|
||||||
|
diff.removeAll(services_ids);
|
||||||
|
System.out.println(diff);
|
||||||
|
if (!diff.isEmpty()) {
|
||||||
|
for (Long forward_id : diff) {
|
||||||
|
Forward forward = forwardService.getById(forward_id);
|
||||||
|
if (forward != null) {
|
||||||
|
forwardService.updateForwardA(forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}, "同步限流器 ");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全执行操作,捕获异常
|
* 安全执行操作,捕获异常
|
||||||
*/
|
*/
|
||||||
@@ -246,6 +250,7 @@ public class CheckGostConfigAsync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析服务名称
|
* 解析服务名称
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user