feat(ja4ebpf): add multi-interface TC, LPM_TRIE ignore_src, unit tests, and fix bugs
- Add multi-interface TC attachment (default "any" = all UP interfaces) - Add BPF LPM_TRIE map ignored_src for kernel-side CIDR filtering - Add userspace ignore_src filtering for SSL/accept4 path via net.IPNet.Contains() - Add AcceptCache for fd→SessionKey correlation with TTL and Close() - Add 5 test files covering writer, procutil, dispatcher, accept_cache, and cmd - Fix formatTCPOptions infinite loop on EOL (case 0 break→return) - Fix pseudoOrderToShort panic on empty slice (negative cap) - Fix AcceptCache goroutine leak (add done channel + Close()) - Update config.yml.example with interfaces, listen_ports, ignore_src - Rewrite docs/services/ja4ebpf.md (was massively stale: XDP, RingBuffer, etc.) - Fix stale XDP/RingBuffer references in docs/architecture.md, thesis, tls.go Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
64
services/ja4ebpf/internal/dispatcher/dispatcher_test.go
Normal file
64
services/ja4ebpf/internal/dispatcher/dispatcher_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package dispatcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassify(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want Protocol
|
||||
}{
|
||||
{"empty", nil, ProtoUnknown},
|
||||
{"empty slice", []byte{}, ProtoUnknown},
|
||||
{"GET request", []byte("GET / HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"POST request", []byte("POST /api HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"PUT request", []byte("PUT /resource HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"DELETE request", []byte("DELETE /item HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"HEAD request", []byte("HEAD / HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"OPTIONS request", []byte("OPTIONS * HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"PATCH request", []byte("PATCH /data HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"CONNECT request", []byte("CONNECT host:443 HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"TRACE request", []byte("TRACE / HTTP/1.1\r\n"), ProtoHTTP1},
|
||||
{"HTTP/2 preface", []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"), ProtoHTTP2},
|
||||
{"HTTP/2 with frames", append([]byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"), []byte{0, 0, 0, 4, 0, 0, 0, 0, 0}...), ProtoHTTP2},
|
||||
{"partial H2 preface", []byte("PRI * HTTP"), ProtoHTTP2},
|
||||
{"garbage", []byte{0x15, 0x03, 0x01, 0x00}, ProtoUnknown},
|
||||
{"TLS record", []byte{0x16, 0x03, 0x01, 0x00, 0x80}, ProtoUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Classify(tt.data)
|
||||
if got != tt.want {
|
||||
t.Errorf("Classify(%q) = %v, want %v", tt.data[:min(len(tt.data), 30)], got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassify_PartialH2Preface(t *testing.T) {
|
||||
// A fragment that is a prefix of the H2 magic but shorter
|
||||
fragment := []byte("PRI * HTTP/2.0\r\n\r\nS")
|
||||
got := Classify(fragment)
|
||||
if got != ProtoHTTP2 {
|
||||
t.Errorf("Classify(partial H2 preface) = %v, want ProtoHTTP2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b, want int
|
||||
}{
|
||||
{1, 2, 1},
|
||||
{5, 3, 3},
|
||||
{0, 0, 0},
|
||||
{-1, 1, -1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := minInt(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("minInt(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user