github.com/chainreactors/fingers@v1.2.1/fingers/optimization_bench_test.go (about) 1 package fingers 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "regexp" 12 "runtime" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/chainreactors/utils/ahocorasick" 18 re2 "github.com/wasilibs/go-re2" 19 ) 20 21 // ────────────────────────────────────────────────────────────── 22 // 1. Pattern Statistics — understand what we're optimizing 23 // ────────────────────────────────────────────────────────────── 24 25 func TestFingerPatternStatistics(t *testing.T) { 26 engine := newPerfEngine(t) 27 28 var totalBody, totalHeader, totalRegexp, totalVuln, totalVersion, totalMD5, totalMMH3, totalCert int 29 var totalFingers, totalRules int 30 var fingersWithBody, fingersWithHeader, fingersWithRegexp int 31 32 for _, finger := range engine.HTTPFingers { 33 totalFingers++ 34 hasBody, hasHeader, hasRegexp := false, false, false 35 for _, rule := range finger.Rules { 36 totalRules++ 37 if rule.Regexps == nil { 38 continue 39 } 40 totalBody += len(rule.Regexps.Body) 41 totalHeader += len(rule.Regexps.Header) 42 totalRegexp += len(rule.Regexps.Regexp) 43 totalVuln += len(rule.Regexps.Vuln) 44 totalVersion += len(rule.Regexps.Version) 45 totalMD5 += len(rule.Regexps.MD5) 46 totalMMH3 += len(rule.Regexps.MMH3) 47 totalCert += len(rule.Regexps.Cert) 48 49 if len(rule.Regexps.Body) > 0 { 50 hasBody = true 51 } 52 if len(rule.Regexps.Header) > 0 { 53 hasHeader = true 54 } 55 if len(rule.Regexps.Regexp) > 0 || len(rule.Regexps.Vuln) > 0 { 56 hasRegexp = true 57 } 58 } 59 if hasBody { 60 fingersWithBody++ 61 } 62 if hasHeader { 63 fingersWithHeader++ 64 } 65 if hasRegexp { 66 fingersWithRegexp++ 67 } 68 } 69 70 t.Logf("=== HTTP Fingerprint Pattern Statistics ===") 71 t.Logf("Total HTTP fingers: %d", totalFingers) 72 t.Logf("Total rules: %d", totalRules) 73 t.Logf("") 74 t.Logf("Body keywords: %d (fingers with body: %d)", totalBody, fingersWithBody) 75 t.Logf("Header keywords: %d (fingers with header: %d)", totalHeader, fingersWithHeader) 76 t.Logf("Regexp patterns: %d (fingers with regexp: %d)", totalRegexp, fingersWithRegexp) 77 t.Logf("Vuln regexp patterns: %d", totalVuln) 78 t.Logf("Version patterns: %d", totalVersion) 79 t.Logf("MD5 hashes: %d", totalMD5) 80 t.Logf("MMH3 hashes: %d", totalMMH3) 81 t.Logf("Cert patterns: %d", totalCert) 82 t.Logf("") 83 t.Logf("AC candidate keywords (body+header): %d", totalBody+totalHeader) 84 t.Logf("RE2 candidate patterns (regexp+vuln+version): %d", totalRegexp+totalVuln+totalVersion) 85 } 86 87 // ────────────────────────────────────────────────────────────── 88 // 2. Real website data collection & helpers 89 // ────────────────────────────────────────────────────────────── 90 91 type realSiteData struct { 92 Name string 93 Content []byte 94 } 95 96 func fetchRealSite(url string) ([]byte, error) { 97 client := &http.Client{Timeout: 10 * time.Second} 98 resp, err := client.Get(url) 99 if err != nil { 100 return nil, err 101 } 102 defer resp.Body.Close() 103 104 var buf bytes.Buffer 105 fmt.Fprintf(&buf, "%s %s\r\n", resp.Proto, resp.Status) 106 for key, values := range resp.Header { 107 for _, v := range values { 108 fmt.Fprintf(&buf, "%s: %s\r\n", key, v) 109 } 110 } 111 buf.WriteString("\r\n") 112 io.Copy(&buf, resp.Body) 113 return buf.Bytes(), nil 114 } 115 116 var testSites = []struct { 117 Name string 118 URL string 119 }{ 120 {"nginx-default", "http://nginx.org"}, 121 {"apache-httpd", "http://httpd.apache.org"}, 122 {"github", "https://github.com"}, 123 {"cloudflare", "https://www.cloudflare.com"}, 124 {"wordpress", "https://wordpress.org"}, 125 {"jenkins", "https://www.jenkins.io"}, 126 {"grafana", "https://grafana.com"}, 127 {"elastic", "https://www.elastic.co"}, 128 } 129 130 func loadTestSites(t *testing.T) []realSiteData { 131 t.Helper() 132 cacheFile := "/tmp/fingers_bench_sites.json.gz" 133 134 if data, err := os.ReadFile(cacheFile); err == nil { 135 gr, err := gzip.NewReader(bytes.NewReader(data)) 136 if err == nil { 137 defer gr.Close() 138 var sites []realSiteData 139 if err := json.NewDecoder(gr).Decode(&sites); err == nil && len(sites) > 0 { 140 t.Logf("loaded %d cached sites from %s", len(sites), cacheFile) 141 return sites 142 } 143 } 144 } 145 146 var sites []realSiteData 147 for _, s := range testSites { 148 content, err := fetchRealSite(s.URL) 149 if err != nil { 150 t.Logf("SKIP %s: %v", s.Name, err) 151 continue 152 } 153 sites = append(sites, realSiteData{Name: s.Name, Content: content}) 154 t.Logf("fetched %s: %d bytes", s.Name, len(content)) 155 } 156 157 if len(sites) > 0 { 158 var buf bytes.Buffer 159 gw := gzip.NewWriter(&buf) 160 json.NewEncoder(gw).Encode(sites) 161 gw.Close() 162 os.WriteFile(cacheFile, buf.Bytes(), 0644) 163 } 164 165 return sites 166 } 167 168 func generateSyntheticResponses() []realSiteData { 169 return []realSiteData{ 170 { 171 Name: "typical-nginx", 172 Content: []byte("HTTP/1.1 200 OK\r\nServer: nginx/1.18.0\r\nContent-Type: text/html\r\nX-Powered-By: PHP/7.4.3\r\n\r\n<!DOCTYPE html><html><head><title>Welcome to nginx!</title></head><body><h1>Welcome to nginx!</h1><p>If you see this page, the nginx web server is successfully installed.</p></body></html>"), 173 }, 174 { 175 Name: "wordpress-site", 176 Content: []byte("HTTP/1.1 200 OK\r\nServer: Apache/2.4.41\r\nX-Powered-By: PHP/7.4.3\r\nLink: <https://example.com/wp-json/>; rel=\"https://api.w.org/\"\r\n\r\n<!DOCTYPE html><html lang=\"en-US\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link rel=\"stylesheet\" href=\"/wp-content/themes/flavor/style.css\"><script src=\"/wp-includes/js/jquery/jquery.min.js\"></script></head><body class=\"home blog wp-custom-logo\"><div id=\"page\"><header><h1 class=\"site-title\"><a href=\"/\">Example Blog</a></h1></header><main><article class=\"post\"><h2>Hello World</h2><p>Welcome to WordPress.</p></article></main><footer><p>Powered by WordPress</p></footer></div></body></html>"), 177 }, 178 { 179 Name: "spring-boot-app", 180 Content: []byte("HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nContent-Type: application/json\r\nX-Application-Context: application:8080\r\n\r\n{\"status\":\"UP\",\"components\":{\"db\":{\"status\":\"UP\",\"details\":{\"database\":\"MySQL\",\"validationQuery\":\"isValid()\"}},\"diskSpace\":{\"status\":\"UP\",\"details\":{\"total\":107374182400,\"free\":53687091200,\"threshold\":10485760,\"path\":\"/opt/app/.\",\"exists\":true}}},\"groups\":[\"liveness\",\"readiness\"]}"), 181 }, 182 { 183 Name: "tomcat-default", 184 Content: []byte("HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nContent-Type: text/html;charset=UTF-8\r\n\r\n<!DOCTYPE html><html lang=\"en\"><head><title>Apache Tomcat/9.0.50</title></head><body><h1>Apache Tomcat/9.0.50</h1><p>If you're seeing this, you've successfully installed Tomcat.</p><ul><li><a href=\"/manager/html\">Server Status</a></li><li><a href=\"/host-manager/html\">Host Manager</a></li></ul></body></html>"), 185 }, 186 { 187 Name: "iis-aspnet", 188 Content: []byte("HTTP/1.1 200 OK\r\nServer: Microsoft-IIS/10.0\r\nX-Powered-By: ASP.NET\r\nX-AspNet-Version: 4.0.30319\r\n\r\n<!DOCTYPE html><html><head><title>IIS Windows Server</title></head><body><img src=\"iisstart.png\" alt=\"IIS\"><h1>Internet Information Services</h1></body></html>"), 189 }, 190 { 191 Name: "large-react-app", 192 Content: generateLargeResponse(64 * 1024), 193 }, 194 { 195 Name: "very-large-response", 196 Content: generateLargeResponse(256 * 1024), 197 }, 198 { 199 Name: "empty-body", 200 Content: []byte("HTTP/1.1 302 Found\r\nServer: cloudflare\r\nLocation: /login\r\nCF-RAY: abc123\r\n\r\n"), 201 }, 202 } 203 } 204 205 func generateLargeResponse(bodySize int) []byte { 206 var buf bytes.Buffer 207 buf.WriteString("HTTP/1.1 200 OK\r\nServer: nginx/1.21.0\r\nContent-Type: text/html\r\nX-Powered-By: Express\r\nSet-Cookie: session=abc123; Path=/; HttpOnly\r\n\r\n") 208 buf.WriteString("<!DOCTYPE html><html><head><title>React App</title>") 209 buf.WriteString("<script src=\"/static/js/main.chunk.js\"></script>") 210 buf.WriteString("<link rel=\"stylesheet\" href=\"/static/css/main.chunk.css\">") 211 buf.WriteString("</head><body><div id=\"root\">") 212 213 filler := "<div class=\"component\"><span>Content block with various text including keywords that may partially match fingerprints like powered-by, server-info, and x-generator headers embedded in HTML.</span></div>\n" 214 for buf.Len() < bodySize { 215 buf.WriteString(filler) 216 } 217 buf.WriteString("</div></body></html>") 218 return buf.Bytes() 219 } 220 221 // ────────────────────────────────────────────────────────────── 222 // 3. Aho-Corasick Optimization Prototype 223 // ────────────────────────────────────────────────────────────── 224 225 type acIndex struct { 226 bodyAC *ahocorasick.Automaton 227 headerAC *ahocorasick.Automaton 228 bodyPatterns []acPatternRef 229 headerPatterns []acPatternRef 230 } 231 232 type acPatternRef struct { 233 FingerIdx int 234 RuleIdx int 235 PatternIdx int 236 Pattern string 237 } 238 239 func buildACIndex(fingers Fingers) *acIndex { 240 idx := &acIndex{} 241 242 var bodyKeywords []string 243 var headerKeywords []string 244 245 for fi, finger := range fingers { 246 for ri, rule := range finger.Rules { 247 if rule.Regexps == nil { 248 continue 249 } 250 for pi, body := range rule.Regexps.Body { 251 bodyKeywords = append(bodyKeywords, body) 252 idx.bodyPatterns = append(idx.bodyPatterns, acPatternRef{ 253 FingerIdx: fi, 254 RuleIdx: ri, 255 PatternIdx: pi, 256 Pattern: body, 257 }) 258 } 259 for pi, header := range rule.Regexps.Header { 260 headerKeywords = append(headerKeywords, header) 261 idx.headerPatterns = append(idx.headerPatterns, acPatternRef{ 262 FingerIdx: fi, 263 RuleIdx: ri, 264 PatternIdx: pi, 265 Pattern: header, 266 }) 267 } 268 } 269 } 270 271 if len(bodyKeywords) > 0 { 272 ac, err := ahocorasick.NewBuilder(). 273 AddStrings(bodyKeywords). 274 Build() 275 if err == nil { 276 idx.bodyAC = ac 277 } 278 } 279 280 if len(headerKeywords) > 0 { 281 ac, err := ahocorasick.NewBuilder(). 282 AddStrings(headerKeywords). 283 Build() 284 if err == nil { 285 idx.headerAC = ac 286 } 287 } 288 289 return idx 290 } 291 292 type acMatchResult struct { 293 FingerIdx int 294 RuleIdx int 295 PatternIdx int 296 MatchType string 297 } 298 299 func (idx *acIndex) matchKeywords(header, body []byte) []acMatchResult { 300 var results []acMatchResult 301 302 if idx.headerAC != nil && len(header) > 0 { 303 matches := idx.headerAC.FindAll(header, -1) 304 for _, m := range matches { 305 ref := idx.headerPatterns[m.PatternID] 306 results = append(results, acMatchResult{ 307 FingerIdx: ref.FingerIdx, 308 RuleIdx: ref.RuleIdx, 309 PatternIdx: ref.PatternIdx, 310 MatchType: "header", 311 }) 312 } 313 } 314 315 if idx.bodyAC != nil && len(body) > 0 { 316 matches := idx.bodyAC.FindAll(body, -1) 317 for _, m := range matches { 318 ref := idx.bodyPatterns[m.PatternID] 319 results = append(results, acMatchResult{ 320 FingerIdx: ref.FingerIdx, 321 RuleIdx: ref.RuleIdx, 322 PatternIdx: ref.PatternIdx, 323 MatchType: "body", 324 }) 325 } 326 } 327 328 return results 329 } 330 331 // ────────────────────────────────────────────────────────────── 332 // 4. Baseline vs AC keyword matching benchmark 333 // ────────────────────────────────────────────────────────────── 334 335 func TestBaselineVsAC_Keywords(t *testing.T) { 336 if testing.Short() { 337 t.Skip("skipping heavy performance comparison in short mode") 338 } 339 engine := newPerfEngine(t) 340 341 sites := generateSyntheticResponses() 342 realSites := loadTestSites(t) 343 sites = append(sites, realSites...) 344 345 if len(sites) == 0 { 346 t.Fatal("no test data available") 347 } 348 349 acIdx := buildACIndex(engine.HTTPFingers) 350 t.Logf("AC index built: %d body patterns, %d header patterns", 351 len(acIdx.bodyPatterns), len(acIdx.headerPatterns)) 352 353 for _, site := range sites { 354 content := NewContent(site.Content, "", true) 355 356 // Baseline: current bytes.Contains approach 357 runtime.GC() 358 baseStart := time.Now() 359 baselineHits := 0 360 for _, finger := range engine.HTTPFingers { 361 for _, rule := range finger.Rules { 362 if rule.Regexps == nil { 363 continue 364 } 365 for _, h := range rule.Regexps.Header { 366 if content.Header != nil && bytes.Contains(content.Header, []byte(h)) { 367 baselineHits++ 368 } 369 } 370 for _, b := range rule.Regexps.Body { 371 body := content.Body 372 if body == nil { 373 body = content.Content 374 } 375 if bytes.Contains(body, []byte(b)) { 376 baselineHits++ 377 } 378 } 379 } 380 } 381 baseElapsed := time.Since(baseStart) 382 383 // AC approach 384 runtime.GC() 385 acStart := time.Now() 386 acResults := acIdx.matchKeywords(content.Header, content.Body) 387 acElapsed := time.Since(acStart) 388 389 speedup := 0.0 390 if acElapsed > 0 { 391 speedup = float64(baseElapsed) / float64(acElapsed) 392 } 393 394 t.Logf("[%s] content=%dB baseline=%s(%d hits) AC=%s(%d hits) speedup=%.2fx", 395 site.Name, len(site.Content), baseElapsed, baselineHits, acElapsed, len(acResults), speedup) 396 } 397 } 398 399 // ────────────────────────────────────────────────────────────── 400 // 5. Baseline vs RE2 regex matching benchmark 401 // ────────────────────────────────────────────────────────────── 402 403 func TestBaselineVsRE2_Regex(t *testing.T) { 404 if testing.Short() { 405 t.Skip("skipping heavy performance comparison in short mode") 406 } 407 engine := newPerfEngine(t) 408 409 // Collect all compiled regexps from HTTP fingers 410 type regInfo struct { 411 Pattern string 412 Compiled CompiledRegexp 413 } 414 var allRegexps []regInfo 415 for _, finger := range engine.HTTPFingers { 416 for _, rule := range finger.Rules { 417 if rule.Regexps == nil { 418 continue 419 } 420 for _, r := range rule.Regexps.CompliedRegexp { 421 allRegexps = append(allRegexps, regInfo{Pattern: r.String(), Compiled: r}) 422 } 423 for _, r := range rule.Regexps.CompiledVulnRegexp { 424 allRegexps = append(allRegexps, regInfo{Pattern: r.String(), Compiled: r}) 425 } 426 } 427 } 428 t.Logf("Total regex patterns to test: %d", len(allRegexps)) 429 430 sites := generateSyntheticResponses() 431 realSites := loadTestSites(t) 432 sites = append(sites, realSites...) 433 434 for _, site := range sites { 435 content := NewContent(site.Content, "", true) 436 437 // Go stdlib regexp (current approach) 438 runtime.GC() 439 goStart := time.Now() 440 goHits := 0 441 for _, ri := range allRegexps { 442 if ri.Compiled.Match(content.Content) { 443 goHits++ 444 } 445 } 446 goElapsed := time.Since(goStart) 447 448 t.Logf("[%s] content=%dB go_regexp=%s(%d hits) patterns=%d", 449 site.Name, len(site.Content), goElapsed, goHits, len(allRegexps)) 450 } 451 } 452 453 // ────────────────────────────────────────────────────────────── 454 // 6. Full engine match: Baseline vs AC-accelerated 455 // ────────────────────────────────────────────────────────────── 456 457 func TestFullEngineMatch_BaselineVsACPrefilter(t *testing.T) { 458 if testing.Short() { 459 t.Skip("skipping heavy performance comparison in short mode") 460 } 461 engine := newPerfEngine(t) 462 acIdx := buildACIndex(engine.HTTPFingers) 463 464 sites := generateSyntheticResponses() 465 realSites := loadTestSites(t) 466 sites = append(sites, realSites...) 467 468 const iterations = 100 469 470 for _, site := range sites { 471 // Baseline: full engine match 472 runtime.GC() 473 baseStart := time.Now() 474 var baseFrames int 475 for i := 0; i < iterations; i++ { 476 fs, _ := engine.HTTPMatch(site.Content, "") 477 baseFrames = len(fs) 478 } 479 baseElapsed := time.Since(baseStart) 480 481 // AC prefilter: identify candidate fingers, then only run those 482 content := NewContent(site.Content, "", true) 483 runtime.GC() 484 acStart := time.Now() 485 var acFrames int 486 for i := 0; i < iterations; i++ { 487 acResults := acIdx.matchKeywords(content.Header, content.Body) 488 489 candidateFingers := make(map[int]bool) 490 for _, r := range acResults { 491 candidateFingers[r.FingerIdx] = true 492 } 493 494 // Still need to run regex fingers that AC can't cover 495 for fi, finger := range engine.HTTPFingers { 496 if candidateFingers[fi] { 497 continue 498 } 499 for _, rule := range finger.Rules { 500 if rule.Regexps == nil { 501 continue 502 } 503 if len(rule.Regexps.CompliedRegexp) > 0 || len(rule.Regexps.CompiledVulnRegexp) > 0 || 504 len(rule.Regexps.MD5) > 0 || len(rule.Regexps.MMH3) > 0 || len(rule.Regexps.Cert) > 0 { 505 candidateFingers[fi] = true 506 break 507 } 508 } 509 } 510 511 count := 0 512 for fi := range candidateFingers { 513 finger := engine.HTTPFingers[fi] 514 _, _, ok := finger.PassiveMatch(content) 515 if ok { 516 count++ 517 } 518 } 519 acFrames = count 520 } 521 acElapsed := time.Since(acStart) 522 523 speedup := 0.0 524 if acElapsed > 0 { 525 speedup = float64(baseElapsed) / float64(acElapsed) 526 } 527 528 t.Logf("[%s] %dx: baseline=%s(%d frameworks) AC_prefilter=%s(%d frameworks) speedup=%.2fx", 529 site.Name, iterations, baseElapsed, baseFrames, acElapsed, acFrames, speedup) 530 } 531 } 532 533 // ────────────────────────────────────────────────────────────── 534 // 7. Formal Benchmarks 535 // ────────────────────────────────────────────────────────────── 536 537 func BenchmarkKeywords_Baseline(b *testing.B) { 538 engine := newPerfEngine(b) 539 content := NewContent(generateLargeResponse(32*1024), "", true) 540 541 b.ReportAllocs() 542 b.ResetTimer() 543 for i := 0; i < b.N; i++ { 544 for _, finger := range engine.HTTPFingers { 545 for _, rule := range finger.Rules { 546 if rule.Regexps == nil { 547 continue 548 } 549 for _, h := range rule.Regexps.Header { 550 if content.Header != nil { 551 bytes.Contains(content.Header, []byte(h)) 552 } 553 } 554 for _, bo := range rule.Regexps.Body { 555 body := content.Body 556 if body == nil { 557 body = content.Content 558 } 559 bytes.Contains(body, []byte(bo)) 560 } 561 } 562 } 563 } 564 } 565 566 func BenchmarkKeywords_AhoCorasick(b *testing.B) { 567 engine := newPerfEngine(b) 568 acIdx := buildACIndex(engine.HTTPFingers) 569 content := NewContent(generateLargeResponse(32*1024), "", true) 570 571 b.ReportAllocs() 572 b.ResetTimer() 573 for i := 0; i < b.N; i++ { 574 acIdx.matchKeywords(content.Header, content.Body) 575 } 576 } 577 578 func BenchmarkRegexp_GoStdlib(b *testing.B) { 579 engine := newPerfEngine(b) 580 var allRegexps []CompiledRegexp 581 for _, finger := range engine.HTTPFingers { 582 for _, rule := range finger.Rules { 583 if rule.Regexps == nil { 584 continue 585 } 586 allRegexps = append(allRegexps, rule.Regexps.CompliedRegexp...) 587 allRegexps = append(allRegexps, rule.Regexps.CompiledVulnRegexp...) 588 } 589 } 590 591 content := NewContent(generateLargeResponse(32*1024), "", true) 592 593 b.ReportAllocs() 594 b.ResetTimer() 595 for i := 0; i < b.N; i++ { 596 for _, r := range allRegexps { 597 r.Match(content.Content) 598 } 599 } 600 } 601 602 func BenchmarkFullMatch_NoAC(b *testing.B) { 603 engine := newPerfEngine(b) 604 content := NewContent(generateLargeResponse(32*1024), "", true) 605 606 b.ReportAllocs() 607 b.ResetTimer() 608 for i := 0; i < b.N; i++ { 609 engine.HTTPFingers.PassiveMatch(content, false) 610 } 611 } 612 613 func BenchmarkFullMatch_WithAC(b *testing.B) { 614 engine := newPerfEngine(b) 615 content := generateLargeResponse(32 * 1024) 616 617 b.ReportAllocs() 618 b.ResetTimer() 619 for i := 0; i < b.N; i++ { 620 engine.HTTPMatch(content, "") 621 } 622 } 623 624 func BenchmarkFullMatch_ACPrefilter(b *testing.B) { 625 engine := newPerfEngine(b) 626 acIdx := buildACIndex(engine.HTTPFingers) 627 raw := generateLargeResponse(32 * 1024) 628 content := NewContent(raw, "", true) 629 630 b.ReportAllocs() 631 b.ResetTimer() 632 for i := 0; i < b.N; i++ { 633 acResults := acIdx.matchKeywords(content.Header, content.Body) 634 candidateFingers := make(map[int]bool) 635 for _, r := range acResults { 636 candidateFingers[r.FingerIdx] = true 637 } 638 for fi, finger := range engine.HTTPFingers { 639 if candidateFingers[fi] { 640 continue 641 } 642 for _, rule := range finger.Rules { 643 if rule.Regexps == nil { 644 continue 645 } 646 if len(rule.Regexps.CompliedRegexp) > 0 || len(rule.Regexps.CompiledVulnRegexp) > 0 || 647 len(rule.Regexps.MD5) > 0 || len(rule.Regexps.MMH3) > 0 || len(rule.Regexps.Cert) > 0 { 648 candidateFingers[fi] = true 649 break 650 } 651 } 652 } 653 for fi := range candidateFingers { 654 engine.HTTPFingers[fi].PassiveMatch(content) 655 } 656 } 657 } 658 659 // ────────────────────────────────────────────────────────────── 660 // 8. Content size scaling analysis 661 // ────────────────────────────────────────────────────────────── 662 663 func TestContentSizeScaling(t *testing.T) { 664 if testing.Short() { 665 t.Skip("skipping heavy performance comparison in short mode") 666 } 667 engine := newPerfEngine(t) 668 acIdx := buildACIndex(engine.HTTPFingers) 669 670 sizes := []int{1024, 4096, 16384, 65536, 262144} 671 const iterations = 50 672 673 t.Logf("%-10s %15s %15s %10s", "Size", "Baseline", "AC", "Speedup") 674 t.Logf(strings.Repeat("-", 55)) 675 676 for _, size := range sizes { 677 raw := generateLargeResponse(size) 678 content := NewContent(raw, "", true) 679 680 // baseline 681 runtime.GC() 682 baseStart := time.Now() 683 for i := 0; i < iterations; i++ { 684 engine.HTTPMatch(raw, "") 685 } 686 baseElapsed := time.Since(baseStart) / time.Duration(iterations) 687 688 // AC 689 runtime.GC() 690 acStart := time.Now() 691 for i := 0; i < iterations; i++ { 692 acResults := acIdx.matchKeywords(content.Header, content.Body) 693 candidateFingers := make(map[int]bool) 694 for _, r := range acResults { 695 candidateFingers[r.FingerIdx] = true 696 } 697 for fi, finger := range engine.HTTPFingers { 698 if candidateFingers[fi] { 699 continue 700 } 701 for _, rule := range finger.Rules { 702 if rule.Regexps == nil { 703 continue 704 } 705 if len(rule.Regexps.CompliedRegexp) > 0 || len(rule.Regexps.CompiledVulnRegexp) > 0 || 706 len(rule.Regexps.MD5) > 0 || len(rule.Regexps.MMH3) > 0 { 707 candidateFingers[fi] = true 708 break 709 } 710 } 711 } 712 for fi := range candidateFingers { 713 engine.HTTPFingers[fi].PassiveMatch(content) 714 } 715 } 716 acElapsed := time.Since(acStart) / time.Duration(iterations) 717 718 _ = acStart 719 speedup := float64(baseElapsed) / float64(acElapsed) 720 t.Logf("%-10d %15s %15s %9.2fx", size, baseElapsed, acElapsed, speedup) 721 } 722 } 723 724 // ────────────────────────────────────────────────────────────── 725 // 9. AC build cost analysis 726 // ────────────────────────────────────────────────────────────── 727 728 func TestACBuildCost(t *testing.T) { 729 engine := newPerfEngine(t) 730 731 start := time.Now() 732 acIdx := buildACIndex(engine.HTTPFingers) 733 buildTime := time.Since(start) 734 735 t.Logf("AC build time: %s", buildTime) 736 t.Logf("Body patterns: %d, Header patterns: %d", len(acIdx.bodyPatterns), len(acIdx.headerPatterns)) 737 738 // Single match warmup + timing 739 raw := generateLargeResponse(16 * 1024) 740 content := NewContent(raw, "", true) 741 742 matchStart := time.Now() 743 results := acIdx.matchKeywords(content.Header, content.Body) 744 matchTime := time.Since(matchStart) 745 746 t.Logf("Single AC match time: %s, hits: %d", matchTime, len(results)) 747 t.Logf("Build amortized over 1000 matches: %s per match", buildTime/1000) 748 } 749 750 // ────────────────────────────────────────────────────────────── 751 // 10. Go stdlib regexp vs go-re2 (C wrapper) comparison 752 // ────────────────────────────────────────────────────────────── 753 754 func TestGoRegexpVsRE2(t *testing.T) { 755 if testing.Short() { 756 t.Skip("skipping heavy performance comparison in short mode") 757 } 758 engine := newPerfEngine(t) 759 760 type patternPair struct { 761 goRe *regexp.Regexp 762 re2Re *re2.Regexp 763 src string 764 } 765 766 var patterns []patternPair 767 for _, finger := range engine.HTTPFingers { 768 for _, rule := range finger.Rules { 769 if rule.Regexps == nil { 770 continue 771 } 772 for _, r := range rule.Regexps.CompliedRegexp { 773 src := r.String() 774 goRe, err := regexp.Compile(src) 775 if err != nil { 776 continue 777 } 778 re2Re, err := re2.Compile(src) 779 if err != nil { 780 t.Logf("SKIP re2 compile %q: %v", src, err) 781 continue 782 } 783 patterns = append(patterns, patternPair{goRe: goRe, re2Re: re2Re, src: src}) 784 } 785 for _, r := range rule.Regexps.CompiledVulnRegexp { 786 src := r.String() 787 goRe, err := regexp.Compile(src) 788 if err != nil { 789 continue 790 } 791 re2Re, err := re2.Compile(src) 792 if err != nil { 793 t.Logf("SKIP re2 compile %q: %v", src, err) 794 continue 795 } 796 patterns = append(patterns, patternPair{goRe: goRe, re2Re: re2Re, src: src}) 797 } 798 } 799 } 800 t.Logf("Compiled %d patterns for both engines", len(patterns)) 801 802 sizes := []int{1024, 8192, 32768, 131072} 803 const iterations = 20 804 805 for _, size := range sizes { 806 content := NewContent(generateLargeResponse(size), "", true) 807 808 // Go stdlib 809 runtime.GC() 810 goStart := time.Now() 811 goHits := 0 812 for i := 0; i < iterations; i++ { 813 for _, p := range patterns { 814 if p.goRe.Match(content.Content) { 815 goHits++ 816 } 817 } 818 } 819 goElapsed := time.Since(goStart) / time.Duration(iterations) 820 821 // go-re2 822 runtime.GC() 823 re2Start := time.Now() 824 re2Hits := 0 825 for i := 0; i < iterations; i++ { 826 for _, p := range patterns { 827 if p.re2Re.Match(content.Content) { 828 re2Hits++ 829 } 830 } 831 } 832 re2Elapsed := time.Since(re2Start) / time.Duration(iterations) 833 834 speedup := float64(goElapsed) / float64(re2Elapsed) 835 t.Logf("[%dB] go_regexp=%s(%d) re2=%s(%d) speedup=%.2fx", 836 size, goElapsed, goHits/iterations, re2Elapsed, re2Hits/iterations, speedup) 837 } 838 } 839 840 func BenchmarkRegexp_RE2(b *testing.B) { 841 engine := newPerfEngine(b) 842 var re2Regexps []*re2.Regexp 843 for _, finger := range engine.HTTPFingers { 844 for _, rule := range finger.Rules { 845 if rule.Regexps == nil { 846 continue 847 } 848 for _, r := range rule.Regexps.CompliedRegexp { 849 re2r, err := re2.Compile(r.String()) 850 if err != nil { 851 continue 852 } 853 re2Regexps = append(re2Regexps, re2r) 854 } 855 for _, r := range rule.Regexps.CompiledVulnRegexp { 856 re2r, err := re2.Compile(r.String()) 857 if err != nil { 858 continue 859 } 860 re2Regexps = append(re2Regexps, re2r) 861 } 862 } 863 } 864 865 content := NewContent(generateLargeResponse(32*1024), "", true) 866 867 b.ReportAllocs() 868 b.ResetTimer() 869 for i := 0; i < b.N; i++ { 870 for _, r := range re2Regexps { 871 r.Match(content.Content) 872 } 873 } 874 } 875 876 // ────────────────────────────────────────────────────────────── 877 // 11. Combined AC + RE2 full optimization 878 // ────────────────────────────────────────────────────────────── 879 880 func TestCombinedOptimization(t *testing.T) { 881 if testing.Short() { 882 t.Skip("skipping heavy performance comparison in short mode") 883 } 884 engine := newPerfEngine(t) 885 acIdx := buildACIndex(engine.HTTPFingers) 886 887 // Pre-compile re2 versions of all regex patterns 888 type re2RulePatterns struct { 889 regexps []*re2.Regexp 890 vulnRegexps []*re2.Regexp 891 } 892 re2Cache := make(map[*Rule]*re2RulePatterns) 893 for _, finger := range engine.HTTPFingers { 894 for _, rule := range finger.Rules { 895 if rule.Regexps == nil { 896 continue 897 } 898 rp := &re2RulePatterns{} 899 for _, r := range rule.Regexps.CompliedRegexp { 900 re2r, err := re2.Compile(r.String()) 901 if err == nil { 902 rp.regexps = append(rp.regexps, re2r) 903 } 904 } 905 for _, r := range rule.Regexps.CompiledVulnRegexp { 906 re2r, err := re2.Compile(r.String()) 907 if err == nil { 908 rp.vulnRegexps = append(rp.vulnRegexps, re2r) 909 } 910 } 911 re2Cache[rule] = rp 912 } 913 } 914 915 sites := generateSyntheticResponses() 916 realSites := loadTestSites(t) 917 sites = append(sites, realSites...) 918 919 iterations := 50 920 for _, site := range sites { 921 if len(site.Content) > 300000 { 922 iterations = 10 923 } 924 925 content := NewContent(site.Content, "", true) 926 927 // Baseline 928 runtime.GC() 929 baseStart := time.Now() 930 var baseFrames int 931 for i := 0; i < iterations; i++ { 932 fs, _ := engine.HTTPMatch(site.Content, "") 933 baseFrames = len(fs) 934 } 935 baseElapsed := time.Since(baseStart) / time.Duration(iterations) 936 937 // AC + RE2 combined 938 runtime.GC() 939 optStart := time.Now() 940 var optFrames int 941 for i := 0; i < iterations; i++ { 942 // Phase 1: AC keyword scan 943 acResults := acIdx.matchKeywords(content.Header, content.Body) 944 matched := make(map[int]bool) 945 for _, r := range acResults { 946 matched[r.FingerIdx] = true 947 } 948 949 // Phase 2: RE2 regex scan for fingerprints with regex-only rules 950 for fi, finger := range engine.HTTPFingers { 951 if matched[fi] { 952 continue 953 } 954 for _, rule := range finger.Rules { 955 if rule.Regexps == nil { 956 continue 957 } 958 rp := re2Cache[rule] 959 if rp == nil { 960 continue 961 } 962 hit := false 963 for _, r := range rp.vulnRegexps { 964 if r.Match(content.Content) { 965 hit = true 966 break 967 } 968 } 969 if !hit { 970 for _, r := range rp.regexps { 971 if r.Match(content.Content) { 972 hit = true 973 break 974 } 975 } 976 } 977 if hit { 978 matched[fi] = true 979 break 980 } 981 } 982 } 983 984 count := 0 985 for fi := range matched { 986 finger := engine.HTTPFingers[fi] 987 _, _, ok := finger.PassiveMatch(content) 988 if ok { 989 count++ 990 } 991 } 992 optFrames = count 993 } 994 optElapsed := time.Since(optStart) / time.Duration(iterations) 995 996 speedup := float64(baseElapsed) / float64(optElapsed) 997 t.Logf("[%s] %dB: baseline=%s(%d) AC+RE2=%s(%d) speedup=%.2fx", 998 site.Name, len(site.Content), baseElapsed, baseFrames, optElapsed, optFrames, speedup) 999 } 1000 }