Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.27'

- run: go vet ./...

- name: staticcheck
uses: dominikh/staticcheck-action@v1.3.0
uses: dominikh/staticcheck-action@v1.4.1
with:
version: '2026.2.1'
install-go: false

tests:
Expand All @@ -42,6 +43,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.27'

- run: go test ./...
- run: go test -race -shuffle=on ./...
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
go-version: "1.27"

- name: Build
env:
Expand Down
14 changes: 7 additions & 7 deletions analyzer/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ type Analyzer interface {
}

type Logger interface {
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Errorf(format string, args ...interface{})
Debugf(format string, args ...any)
Infof(format string, args ...any)
Errorf(format string, args ...any)
}

type TCPAnalyzer interface {
Expand Down Expand Up @@ -82,19 +82,19 @@ type UDPStream interface {
}

type (
PropMap map[string]interface{}
PropMap map[string]any
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{} {
func (m PropMap) Get(key string) any {
keys := strings.Split(key, ".")
if len(keys) == 0 {
return nil
}
var current interface{} = m
var current any = m
for _, k := range keys {
currentMap, ok := current.(PropMap)
if !ok {
Expand All @@ -108,7 +108,7 @@ func (m PropMap) Get(key string) interface{} {
// 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{} {
func (cm CombinedPropMap) Get(an string, key string) any {
m, ok := cm[an]
if !ok {
return nil
Expand Down
26 changes: 8 additions & 18 deletions analyzer/tcp/fet.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package tcp

import "github.com/apernet/OpenGFW/analyzer"
import (
"math/bits"

"github.com/apernet/OpenGFW/analyzer"
)

var _ analyzer.TCPAnalyzer = (*FETAnalyzer)(nil)

Expand Down Expand Up @@ -61,15 +65,6 @@ 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 {
Expand All @@ -78,7 +73,7 @@ func averagePopCount(bytes []byte) float32 {
}
total := 0
for _, b := range bytes {
total += popCount(b)
total += bits.OnesCount8(b)
}
return float32(total) / float32(len(bytes))
}
Expand Down Expand Up @@ -125,16 +120,11 @@ func contiguousPrintable(bytes []byte) int {
if isPrintable(bytes[i]) {
current++
} else {
if current > maxCount {
maxCount = current
}
maxCount = max(maxCount, current)
current = 0
}
}
if current > maxCount {
maxCount = current
}
return maxCount
return max(maxCount, current)
}

// isTLSorHTTP returns true if the given bytes look like TLS or HTTP.
Expand Down
42 changes: 42 additions & 0 deletions analyzer/tcp/fet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package tcp

import "testing"

func TestAveragePopCount(t *testing.T) {
tests := []struct {
name string
data []byte
want float32
}{
{name: "empty", want: 0},
{name: "zero", data: []byte{0, 0}, want: 0},
{name: "full", data: []byte{0xff}, want: 8},
{name: "mixed", data: []byte{0x00, 0xff}, want: 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := averagePopCount(tt.data); got != tt.want {
t.Fatalf("averagePopCount(%v) = %v, want %v", tt.data, got, tt.want)
}
})
}
}

func TestContiguousPrintable(t *testing.T) {
tests := []struct {
name string
data []byte
want int
}{
{name: "empty"},
{name: "middle", data: []byte{0, 'a', 'b', 0, 'c'}, want: 2},
{name: "end", data: []byte{'a', 0, 'b', 'c', 'd'}, want: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := contiguousPrintable(tt.data); got != tt.want {
t.Fatalf("contiguousPrintable(%v) = %d, want %d", tt.data, got, tt.want)
}
})
}
}
10 changes: 4 additions & 6 deletions analyzer/tcp/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,16 +152,14 @@ func (s *httpStream) parseHeaders(buf *utils.ByteBuffer) (utils.LSMAction, analy
}
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 {
for line := range bytes.SplitSeq(headers, []byte("\r\n")) {
key, value, ok := bytes.Cut(line, []byte(":"))
if !ok {
// 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
headerMap[strings.ToLower(string(bytes.TrimSpace(key)))] = string(bytes.TrimSpace(value))
}
return utils.LSMActionNext, headerMap
}
Expand Down
4 changes: 4 additions & 0 deletions analyzer/tcp/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ func TestHTTPParsing_Request(t *testing.T) {
"PUT /world HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody": {
"method": "PUT", "path": "/world", "version": "HTTP/1.1", "headers": analyzer.PropMap{"content-length": "4"},
},
"GET /example HTTP/1.1\r\nHost: example.com\r\nX-Request-ID: a:b\r\n\r\n": {
"method": "GET", "path": "/example", "version": "HTTP/1.1",
"headers": analyzer.PropMap{"host": "example.com", "x-request-id": "a:b"},
},
"DELETE /goodbye HTTP/2.0\r\n": {
"method": "DELETE", "path": "/goodbye", "version": "HTTP/2.0",
},
Expand Down
2 changes: 1 addition & 1 deletion analyzer/udp/internal/quic/packet_protector.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]by

pnLen := packet[0]&0x3 + 1
pn := int64(0)
for i := uint8(0); i < pnLen; i++ {
for i := range pnLen {
packet[pnOffset:][i] ^= mask[1+i]
pn = (pn << 8) | int64(packet[pnOffset:][i])
}
Expand Down
5 changes: 3 additions & 2 deletions analyzer/udp/internal/quic/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package quic

import (
"bytes"
"cmp"
"crypto"
"errors"
"fmt"
"io"
"sort"
"slices"

"github.com/quic-go/quic-go/quicvarint"
"golang.org/x/crypto/hkdf"
Expand Down Expand Up @@ -106,7 +107,7 @@ func assembleCryptoFrames(frames []cryptoFrame) []byte {
return frames[0].Data
}
// sort the frames by offset
sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })
slices.SortFunc(frames, func(a, b cryptoFrame) int { return cmp.Compare(a.Offset, b.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)) {
Expand Down
24 changes: 24 additions & 0 deletions analyzer/udp/internal/quic/payload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package quic

import (
"bytes"
"testing"
)

func TestAssembleCryptoFrames(t *testing.T) {
frames := []cryptoFrame{
{Offset: 2, Data: []byte("cd")},
{Offset: 0, Data: []byte("ab")},
}
if got := assembleCryptoFrames(frames); !bytes.Equal(got, []byte("abcd")) {
t.Fatalf("assembled frames = %q, want abcd", got)
}

gap := []cryptoFrame{
{Offset: 0, Data: []byte("ab")},
{Offset: 3, Data: []byte("d")},
}
if got := assembleCryptoFrames(gap); got != nil {
t.Fatalf("assembled noncontiguous frames = %q, want nil", got)
}
}
6 changes: 3 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,21 +382,21 @@ func (l *engineLogger) ModifyError(info ruleset.StreamInfo, err error) {
zap.Error(err))
}

func (l *engineLogger) AnalyzerDebugf(streamID int64, name string, format string, args ...interface{}) {
func (l *engineLogger) AnalyzerDebugf(streamID int64, name string, format string, args ...any) {
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{}) {
func (l *engineLogger) AnalyzerInfof(streamID int64, name string, format string, args ...any) {
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{}) {
func (l *engineLogger) AnalyzerErrorf(streamID int64, name string, format string, args ...any) {
logger.Error("analyzer error message",
zap.Int64("id", streamID),
zap.String("name", name),
Expand Down
9 changes: 7 additions & 2 deletions engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package engine
import (
"context"
"runtime"
"sync"

"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
Expand Down Expand Up @@ -58,11 +59,15 @@ func (e *engine) UpdateRuleset(r ruleset.Ruleset) error {

func (e *engine) Run(ctx context.Context) error {
ioCtx, ioCancel := context.WithCancel(ctx)
defer ioCancel() // Stop workers & IO
var workers sync.WaitGroup
defer func() {
ioCancel()
workers.Wait()
}()

// Start workers
for _, w := range e.workers {
go w.Run(ioCtx)
workers.Go(func() { w.Run(ioCtx) })
}

// Register IO callback
Expand Down
Loading