380 lines
10 KiB
Go
380 lines
10 KiB
Go
package engine
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.difuse.io/Difuse/Mellaris/analyzer"
|
|
"git.difuse.io/Difuse/Mellaris/io"
|
|
"git.difuse.io/Difuse/Mellaris/ruleset"
|
|
|
|
"github.com/bwmarrin/snowflake"
|
|
)
|
|
|
|
type fixedRuleset struct {
|
|
action ruleset.Action
|
|
}
|
|
|
|
func (r fixedRuleset) Analyzers(ruleset.StreamInfo) []analyzer.Analyzer {
|
|
return nil
|
|
}
|
|
|
|
func (r fixedRuleset) Match(ruleset.StreamInfo) ruleset.MatchResult {
|
|
return ruleset.MatchResult{Action: r.action}
|
|
}
|
|
|
|
type analyzerRuleset struct {
|
|
action ruleset.Action
|
|
ans []analyzer.Analyzer
|
|
}
|
|
|
|
func (r analyzerRuleset) Analyzers(ruleset.StreamInfo) []analyzer.Analyzer {
|
|
return r.ans
|
|
}
|
|
|
|
func (r analyzerRuleset) Match(ruleset.StreamInfo) ruleset.MatchResult {
|
|
return ruleset.MatchResult{Action: r.action}
|
|
}
|
|
|
|
type countingTCPAnalyzer struct {
|
|
newCalls *int
|
|
feedCalls *int
|
|
}
|
|
|
|
func (a countingTCPAnalyzer) Name() string { return "tls" }
|
|
func (a countingTCPAnalyzer) Limit() int { return 0 }
|
|
func (a countingTCPAnalyzer) NewTCP(analyzer.TCPInfo, analyzer.Logger) analyzer.TCPStream {
|
|
(*a.newCalls)++
|
|
return countingTCPStream{feedCalls: a.feedCalls}
|
|
}
|
|
|
|
type countingTCPStream struct {
|
|
feedCalls *int
|
|
}
|
|
|
|
func (s countingTCPStream) Feed(bool, bool, bool, int, []byte) (*analyzer.PropUpdate, bool) {
|
|
(*s.feedCalls)++
|
|
return nil, false
|
|
}
|
|
|
|
func (s countingTCPStream) Close(bool) *analyzer.PropUpdate {
|
|
return nil
|
|
}
|
|
|
|
type logFinalizingRuleset struct {
|
|
ans []analyzer.Analyzer
|
|
}
|
|
|
|
func (r logFinalizingRuleset) Analyzers(ruleset.StreamInfo) []analyzer.Analyzer {
|
|
return r.ans
|
|
}
|
|
|
|
func (r logFinalizingRuleset) Match(info ruleset.StreamInfo) ruleset.MatchResult {
|
|
if _, ok := info.Props["tls"]; ok {
|
|
return ruleset.MatchResult{Action: ruleset.ActionMaybe, Logged: true}
|
|
}
|
|
return ruleset.MatchResult{Action: ruleset.ActionMaybe}
|
|
}
|
|
|
|
func (r logFinalizingRuleset) CanFinalizeAfterLog(ruleset.StreamInfo, []string) bool {
|
|
return true
|
|
}
|
|
|
|
type requestPropTCPAnalyzer struct {
|
|
closeCalls *int
|
|
}
|
|
|
|
func (a requestPropTCPAnalyzer) Name() string { return "tls" }
|
|
func (a requestPropTCPAnalyzer) Limit() int { return 0 }
|
|
func (a requestPropTCPAnalyzer) NewTCP(analyzer.TCPInfo, analyzer.Logger) analyzer.TCPStream {
|
|
return requestPropTCPStream{closeCalls: a.closeCalls}
|
|
}
|
|
|
|
type requestPropTCPStream struct {
|
|
closeCalls *int
|
|
}
|
|
|
|
func (s requestPropTCPStream) Feed(bool, bool, bool, int, []byte) (*analyzer.PropUpdate, bool) {
|
|
return &analyzer.PropUpdate{
|
|
Type: analyzer.PropUpdateMerge,
|
|
M: analyzer.PropMap{"req": analyzer.PropMap{"sni": "good.example"}},
|
|
}, false
|
|
}
|
|
|
|
func (s requestPropTCPStream) Close(bool) *analyzer.PropUpdate {
|
|
(*s.closeCalls)++
|
|
return nil
|
|
}
|
|
|
|
type noopTestLogger struct{}
|
|
|
|
func (noopTestLogger) WorkerStart(int) {}
|
|
func (noopTestLogger) WorkerStop(int) {}
|
|
|
|
func (noopTestLogger) TCPStreamNew(int, ruleset.StreamInfo) {}
|
|
func (noopTestLogger) TCPStreamPropUpdate(ruleset.StreamInfo, bool) {
|
|
}
|
|
func (noopTestLogger) TCPStreamAction(ruleset.StreamInfo, ruleset.Action, bool) {
|
|
}
|
|
|
|
func (noopTestLogger) UDPStreamNew(int, ruleset.StreamInfo) {}
|
|
func (noopTestLogger) UDPStreamPropUpdate(ruleset.StreamInfo, bool) {
|
|
}
|
|
func (noopTestLogger) UDPStreamAction(ruleset.StreamInfo, ruleset.Action, bool) {
|
|
}
|
|
|
|
func (noopTestLogger) ModifyError(ruleset.StreamInfo, error) {}
|
|
|
|
func (noopTestLogger) AnalyzerDebugf(int64, string, string, ...interface{}) {}
|
|
func (noopTestLogger) AnalyzerInfof(int64, string, string, ...interface{}) {}
|
|
func (noopTestLogger) AnalyzerErrorf(int64, string, string, ...interface{}) {}
|
|
|
|
func TestUDPStreamUsesUpdatedRuleset(t *testing.T) {
|
|
node, err := snowflake.NewNode(0)
|
|
if err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
f := &udpStreamFactory{
|
|
WorkerID: 0,
|
|
Logger: noopTestLogger{},
|
|
Node: node,
|
|
Ruleset: fixedRuleset{action: ruleset.ActionAllow},
|
|
}
|
|
|
|
tuple := udpTupleKey{AIP: [16]byte{10, 0, 0, 1}, BIP: [16]byte{10, 0, 0, 2}, ALen: 4, BLen: 4, APort: 12345, BPort: 53}
|
|
payload := []byte("query")
|
|
ctx := &udpContext{Verdict: udpVerdictAccept}
|
|
s := f.New(tuple, payload, ctx)
|
|
|
|
if err := f.UpdateRuleset(fixedRuleset{action: ruleset.ActionBlock}); err != nil {
|
|
t.Fatalf("update ruleset: %v", err)
|
|
}
|
|
|
|
if !s.Accept(false, ctx) {
|
|
t.Fatalf("unexpected Accept=false for virgin stream")
|
|
}
|
|
s.Feed(false, payload, ctx)
|
|
if ctx.Verdict != udpVerdictDropStream {
|
|
t.Fatalf("verdict=%v want=%v", ctx.Verdict, udpVerdictDropStream)
|
|
}
|
|
}
|
|
|
|
func TestUDPStreamReevaluatesAfterRulesetVersionChange(t *testing.T) {
|
|
node, err := snowflake.NewNode(0)
|
|
if err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
f := &udpStreamFactory{
|
|
WorkerID: 0,
|
|
Logger: noopTestLogger{},
|
|
Node: node,
|
|
Ruleset: fixedRuleset{action: ruleset.ActionAllow},
|
|
}
|
|
|
|
tuple := udpTupleKey{AIP: [16]byte{10, 0, 0, 1}, BIP: [16]byte{10, 0, 0, 2}, ALen: 4, BLen: 4, APort: 12345, BPort: 53}
|
|
payload := []byte("query")
|
|
|
|
ctx1 := &udpContext{Verdict: udpVerdictAccept}
|
|
s := f.New(tuple, payload, ctx1)
|
|
if !s.Accept(false, ctx1) {
|
|
t.Fatalf("unexpected Accept=false before first feed")
|
|
}
|
|
s.Feed(false, payload, ctx1)
|
|
if ctx1.Verdict != udpVerdictAcceptStream {
|
|
t.Fatalf("verdict=%v want=%v", ctx1.Verdict, udpVerdictAcceptStream)
|
|
}
|
|
|
|
if err := f.UpdateRuleset(fixedRuleset{action: ruleset.ActionBlock}); err != nil {
|
|
t.Fatalf("update ruleset: %v", err)
|
|
}
|
|
|
|
ctx2 := &udpContext{Verdict: udpVerdictAccept}
|
|
if !s.Accept(false, ctx2) {
|
|
t.Fatalf("expected Accept=true after ruleset update")
|
|
}
|
|
s.Feed(false, payload, ctx2)
|
|
if ctx2.Verdict != udpVerdictDropStream {
|
|
t.Fatalf("verdict=%v want=%v", ctx2.Verdict, udpVerdictDropStream)
|
|
}
|
|
|
|
ctx3 := &udpContext{Verdict: udpVerdictAccept}
|
|
if s.Accept(false, ctx3) {
|
|
t.Fatalf("expected Accept=false with unchanged ruleset and no active entries")
|
|
}
|
|
if ctx3.Verdict != udpVerdictDropStream {
|
|
t.Fatalf("verdict=%v want=%v", ctx3.Verdict, udpVerdictDropStream)
|
|
}
|
|
}
|
|
|
|
func TestTCPFlowUsesUpdatedRuleset(t *testing.T) {
|
|
node, err := snowflake.NewNode(0)
|
|
if err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
mgr := newTCPFlowManager(0, noopTestLogger{}, nil, node, nil)
|
|
mgr.updateRuleset(fixedRuleset{action: ruleset.ActionAllow}, 0)
|
|
|
|
l3 := L3Info{
|
|
Version: 4,
|
|
Protocol: 6,
|
|
SrcIP: [4]byte{10, 0, 0, 1},
|
|
DstIP: [4]byte{10, 0, 0, 2},
|
|
}
|
|
tcp := TCPInfo{
|
|
SrcPort: 12345,
|
|
DstPort: 443,
|
|
Seq: 100,
|
|
}
|
|
|
|
v := mgr.handle(1, l3, tcp, nil, nil, nil)
|
|
if v != io.VerdictAcceptStream {
|
|
t.Fatalf("first verdict=%v want=%v", v, io.VerdictAcceptStream)
|
|
}
|
|
|
|
mgr.updateRuleset(fixedRuleset{action: ruleset.ActionBlock}, 1)
|
|
|
|
tcp2 := TCPInfo{
|
|
SrcPort: 12345,
|
|
DstPort: 443,
|
|
Seq: 100,
|
|
}
|
|
v = mgr.handle(2, l3, tcp2, []byte("data"), nil, nil)
|
|
if v != io.VerdictDropStream {
|
|
t.Fatalf("verdict after update=%v want=%v", v, io.VerdictDropStream)
|
|
}
|
|
}
|
|
|
|
func TestTCPFlowReevaluatesAfterRulesetVersionChange(t *testing.T) {
|
|
node, err := snowflake.NewNode(0)
|
|
if err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
mgr := newTCPFlowManager(0, noopTestLogger{}, nil, node, nil)
|
|
mgr.updateRuleset(fixedRuleset{action: ruleset.ActionAllow}, 0)
|
|
|
|
l3 := L3Info{
|
|
Version: 4,
|
|
Protocol: 6,
|
|
SrcIP: [4]byte{10, 0, 0, 1},
|
|
DstIP: [4]byte{10, 0, 0, 2},
|
|
}
|
|
tcp := TCPInfo{
|
|
SrcPort: 12345,
|
|
DstPort: 443,
|
|
Seq: 100,
|
|
}
|
|
|
|
v := mgr.handle(1, l3, tcp, nil, nil, nil)
|
|
if v != io.VerdictAcceptStream {
|
|
t.Fatalf("first verdict=%v want=%v", v, io.VerdictAcceptStream)
|
|
}
|
|
|
|
mgr.updateRuleset(fixedRuleset{action: ruleset.ActionBlock}, 1)
|
|
|
|
tcp2 := TCPInfo{
|
|
SrcPort: 12345,
|
|
DstPort: 443,
|
|
Seq: 100,
|
|
}
|
|
v = mgr.handle(2, l3, tcp2, []byte("data"), nil, nil)
|
|
if v != io.VerdictDropStream {
|
|
t.Fatalf("verdict after update=%v want=%v", v, io.VerdictDropStream)
|
|
}
|
|
|
|
tcp3 := TCPInfo{
|
|
SrcPort: 12345,
|
|
DstPort: 443,
|
|
Seq: 104,
|
|
}
|
|
v = mgr.handle(1, l3, tcp3, nil, nil, nil)
|
|
if v != io.VerdictDropStream {
|
|
t.Fatalf("cached verdict after update=%v want=%v", v, io.VerdictDropStream)
|
|
}
|
|
}
|
|
|
|
func TestTCPFlowDelaysAnalyzerCreationUntilPayload(t *testing.T) {
|
|
node, err := snowflake.NewNode(0)
|
|
if err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
newCalls := 0
|
|
feedCalls := 0
|
|
mgr := newTCPFlowManager(0, noopTestLogger{}, nil, node, newAnalyzerSelector(AnalyzerSelectionModeSignature, &statsCounters{}))
|
|
mgr.updateRuleset(analyzerRuleset{
|
|
action: ruleset.ActionMaybe,
|
|
ans: []analyzer.Analyzer{countingTCPAnalyzer{
|
|
newCalls: &newCalls,
|
|
feedCalls: &feedCalls,
|
|
}},
|
|
}, 0)
|
|
|
|
l3 := L3Info{
|
|
Version: 4,
|
|
Protocol: 6,
|
|
SrcIP: [4]byte{10, 0, 0, 1},
|
|
DstIP: [4]byte{10, 0, 0, 2},
|
|
}
|
|
tcp := TCPInfo{
|
|
SrcPort: 12345,
|
|
DstPort: 443,
|
|
Seq: 100,
|
|
}
|
|
|
|
v := mgr.handle(1, l3, tcp, nil, nil, nil)
|
|
if v != io.VerdictAccept {
|
|
t.Fatalf("empty packet verdict=%v want=%v", v, io.VerdictAccept)
|
|
}
|
|
if newCalls != 0 || feedCalls != 0 {
|
|
t.Fatalf("empty packet created/feed analyzer: new=%d feed=%d", newCalls, feedCalls)
|
|
}
|
|
|
|
tcp.Seq = 101
|
|
v = mgr.handle(1, l3, tcp, []byte{0x16, 0x03, 0x01}, nil, nil)
|
|
if v != io.VerdictAccept {
|
|
t.Fatalf("payload verdict=%v want=%v", v, io.VerdictAccept)
|
|
}
|
|
if newCalls != 1 || feedCalls != 1 {
|
|
t.Fatalf("payload should create/feed analyzer once: new=%d feed=%d", newCalls, feedCalls)
|
|
}
|
|
}
|
|
|
|
func TestTCPFlowFinalizesAfterLogClassification(t *testing.T) {
|
|
node, err := snowflake.NewNode(0)
|
|
if err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
closeCalls := 0
|
|
mgr := newTCPFlowManager(0, noopTestLogger{}, nil, node, newAnalyzerSelector(AnalyzerSelectionModeSignature, &statsCounters{}))
|
|
mgr.updateRuleset(logFinalizingRuleset{
|
|
ans: []analyzer.Analyzer{requestPropTCPAnalyzer{closeCalls: &closeCalls}},
|
|
}, 0)
|
|
|
|
l3 := L3Info{
|
|
Version: 4,
|
|
Protocol: 6,
|
|
SrcIP: [4]byte{10, 0, 0, 1},
|
|
DstIP: [4]byte{10, 0, 0, 2},
|
|
}
|
|
tcp := TCPInfo{
|
|
SrcPort: 12345,
|
|
DstPort: 443,
|
|
Seq: 100,
|
|
}
|
|
|
|
v := mgr.handle(1, l3, tcp, nil, nil, nil)
|
|
if v != io.VerdictAccept {
|
|
t.Fatalf("empty packet verdict=%v want=%v", v, io.VerdictAccept)
|
|
}
|
|
|
|
tcp.Seq = 101
|
|
v = mgr.handle(1, l3, tcp, []byte{0x16, 0x03, 0x01}, nil, nil)
|
|
if v != io.VerdictAcceptStream {
|
|
t.Fatalf("payload verdict=%v want=%v", v, io.VerdictAcceptStream)
|
|
}
|
|
if closeCalls != 1 {
|
|
t.Fatalf("expected analyzer to be closed once after finalization, got %d", closeCalls)
|
|
}
|
|
if _, ok := mgr.flows[1]; ok {
|
|
t.Fatal("expected finalized TCP flow to be removed from manager")
|
|
}
|
|
}
|