mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-08-27 01:42:27 +00:00
feat(discovery): Implement hierarchical discovery framework with API and UI integration
- Added types and structures for discovery checks, results, and configurations in `src/discovery/types.go`. - Developed API endpoints for starting, checking status, canceling, and adding presets in `src/http/handler/discovery.go`. - Created request and response types for discovery operations in `src/http/handler/discovery_types.go`. - Built the Discovery UI component to manage domain testing, display results, and handle user interactions in `src/http/ui/src/components/organisms/discovery/Discovery.tsx`. - Integrated the Discovery component into the main Discovery page with appropriate alerts and information in `src/http/ui/src/components/pages/Discovery.tsx`.
This commit is contained in:
parent
6562be9732
commit
0048d89fee
22 changed files with 2146 additions and 2001 deletions
|
|
@ -1,11 +1,12 @@
|
|||
# B4 - Bye Bye Big Bro
|
||||
|
||||
## [1.1x.x] - 2025-1x-xx
|
||||
## [1.19.x] - 2025-1x-xx
|
||||
|
||||
- ADDED: Filter for configuration sets - search by `name`, `SNI` domains, `geosite` categories, or `geoip` categories.
|
||||
- ADDED: Compare sets feature - side-by-side diff view showing differences between two configuration sets, grouped by section (TCP, UDP, Fragmentation, Faking, Targets).
|
||||
- FIXED: Settings tab navigation losing selected tab on page refresh.
|
||||
- CHANGED: New configuration sets are now added to the top of the list instead of the bottom.
|
||||
- CHANGED: `Discovery` configuration refactoring.
|
||||
|
||||
## [1.18.5] - 2025-11-27
|
||||
|
||||
|
|
|
|||
|
|
@ -1,337 +0,0 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/daniellavrushin/b4/nfq"
|
||||
)
|
||||
|
||||
const MAX_PRESETS_PER_DOMAIN = 3
|
||||
const DISCOVERY_TIMEOUT = 3 * time.Second
|
||||
|
||||
type DiscoverySuite struct {
|
||||
*CheckSuite
|
||||
pool *nfq.Pool
|
||||
originalConfig *config.Config
|
||||
presets []ConfigPreset
|
||||
domainResults map[string]*DomainDiscoveryResult // domain -> results for all presets
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDiscoverySuite(checkConfig CheckConfig, pool *nfq.Pool, presets []ConfigPreset) *DiscoverySuite {
|
||||
checkConfig.Timeout = DISCOVERY_TIMEOUT
|
||||
|
||||
suite := NewCheckSuite(checkConfig)
|
||||
suite.DomainDiscoveryResults = make(map[string]*DomainDiscoveryResult)
|
||||
|
||||
return &DiscoverySuite{
|
||||
CheckSuite: suite,
|
||||
pool: pool,
|
||||
presets: presets,
|
||||
domainResults: make(map[string]*DomainDiscoveryResult),
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) RunDiscovery(domains []string) {
|
||||
// Register in activeSuites so status endpoint can find it
|
||||
suitesMu.Lock()
|
||||
activeSuites[ds.Id] = ds.CheckSuite
|
||||
suitesMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
ds.EndTime = time.Now()
|
||||
|
||||
// Keep in memory for 5 minutes
|
||||
time.AfterFunc(5*time.Minute, func() {
|
||||
suitesMu.Lock()
|
||||
delete(activeSuites, ds.Id)
|
||||
suitesMu.Unlock()
|
||||
})
|
||||
}()
|
||||
|
||||
// Set initial status
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.Status = CheckStatusRunning
|
||||
ds.TotalChecks = len(domains) * len(ds.presets)
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
// Store original configuration
|
||||
ds.originalConfig = ds.pool.GetFirstWorkerConfig()
|
||||
if ds.originalConfig == nil {
|
||||
log.Errorf("Failed to get original configuration")
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.Status = CheckStatusFailed
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Starting domain-centric discovery for %d domains across %d presets",
|
||||
len(domains), len(ds.presets))
|
||||
log.Warnf("Service traffic will be affected during discovery testing")
|
||||
|
||||
// Initialize domain results
|
||||
for _, domain := range domains {
|
||||
ds.mu.Lock()
|
||||
ds.domainResults[domain] = &DomainDiscoveryResult{
|
||||
Domain: domain,
|
||||
Results: make(map[string]*DomainPresetResult),
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
select {
|
||||
case <-ds.cancel:
|
||||
log.Infof("Discovery suite %s canceled", ds.Id)
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.Status = CheckStatusCanceled
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
log.Infof("Testing domain: %s (will stop after %d successful configs)", domain, MAX_PRESETS_PER_DOMAIN)
|
||||
|
||||
successfulCount := 0
|
||||
testedCount := 0
|
||||
|
||||
for _, preset := range ds.presets {
|
||||
select {
|
||||
case <-ds.cancel:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Stop testing this domain if we found enough successful configs
|
||||
if successfulCount >= MAX_PRESETS_PER_DOMAIN {
|
||||
log.Infof(" Domain %s: Found %d successful configs, skipping remaining %d presets",
|
||||
domain, successfulCount, len(ds.presets)-testedCount)
|
||||
|
||||
// Update total checks to reflect skipped presets
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.TotalChecks -= (len(ds.presets) - testedCount)
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
testedCount++
|
||||
log.Tracef(" Testing %s with preset %d/%d: %s", domain, testedCount, len(ds.presets), preset.Name)
|
||||
|
||||
// Apply preset configuration by RESTARTING pool
|
||||
testConfig := ds.buildTestConfig(preset, domain)
|
||||
|
||||
log.Infof(" Applying preset %s config...", preset.Name)
|
||||
if err := ds.pool.UpdateConfig(testConfig); err != nil {
|
||||
log.Errorf("Failed to update config for preset %s: %v", preset.Name, err)
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.CompletedChecks++
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// Small delay to let config propagate to all workers
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
|
||||
var result CheckResult
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
result = ds.testDomain(domain)
|
||||
result.Set = testConfig.MainSet
|
||||
|
||||
// If successful or it's the last attempt, use this result
|
||||
if result.Status == CheckStatusComplete || attempt == 1 {
|
||||
break
|
||||
}
|
||||
|
||||
// First attempt failed, wait a bit longer for config to propagate
|
||||
log.Tracef(" First attempt failed, retrying after additional delay...")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Store result for this domain+preset combination
|
||||
ds.mu.Lock()
|
||||
ds.domainResults[domain].Results[preset.Name] = &DomainPresetResult{
|
||||
PresetName: preset.Name,
|
||||
Status: result.Status,
|
||||
Duration: result.Duration,
|
||||
Speed: result.Speed,
|
||||
BytesRead: result.BytesRead,
|
||||
Error: result.Error,
|
||||
StatusCode: result.StatusCode,
|
||||
Set: result.Set,
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
|
||||
// Count successful results
|
||||
if result.Status == CheckStatusComplete {
|
||||
successfulCount++
|
||||
log.Infof(" ✓ %s with %s: %.2f KB/s (success %d/%d)",
|
||||
domain, preset.Name, result.Speed/1024, successfulCount, MAX_PRESETS_PER_DOMAIN)
|
||||
} else {
|
||||
log.Tracef(" ✗ %s with %s: %s",
|
||||
domain, preset.Name, result.Status)
|
||||
}
|
||||
|
||||
// Update progress
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.CompletedChecks++
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
}
|
||||
|
||||
// Determine best preset for this domain
|
||||
ds.determineBestPresetForDomain(domain)
|
||||
|
||||
log.Infof("Domain %s complete: tested %d presets, found %d successful configs",
|
||||
domain, testedCount, successfulCount)
|
||||
}
|
||||
|
||||
// Copy results to CheckSuite for JSON serialization
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.CheckSuite.DomainDiscoveryResults = ds.domainResults
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
// Restore original configuration
|
||||
log.Infof("Restoring original configuration")
|
||||
if err := ds.pool.UpdateConfig(ds.originalConfig); err != nil {
|
||||
log.Errorf("Failed to restore original configuration: %v", err)
|
||||
}
|
||||
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.Status = CheckStatusComplete
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
// Log summary
|
||||
ds.logDiscoverySummary()
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) determineBestPresetForDomain(domain string) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
domainResult := ds.domainResults[domain]
|
||||
if domainResult == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var bestPreset string
|
||||
var bestSpeed float64
|
||||
var bestSuccess bool
|
||||
|
||||
for presetName, result := range domainResult.Results {
|
||||
// Prioritize success status first, then speed
|
||||
isSuccess := result.Status == CheckStatusComplete
|
||||
|
||||
if !bestSuccess && isSuccess {
|
||||
// First successful result
|
||||
bestSuccess = true
|
||||
bestPreset = presetName
|
||||
bestSpeed = result.Speed
|
||||
} else if bestSuccess == isSuccess {
|
||||
// Both succeeded or both failed - compare speed
|
||||
if result.Speed > bestSpeed {
|
||||
bestPreset = presetName
|
||||
bestSpeed = result.Speed
|
||||
}
|
||||
}
|
||||
// If current best is successful but this one failed, skip
|
||||
}
|
||||
|
||||
domainResult.BestPreset = bestPreset
|
||||
domainResult.BestSpeed = bestSpeed
|
||||
domainResult.BestSuccess = bestSuccess
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) buildTestConfig(preset ConfigPreset, testDomain string) *config.Config {
|
||||
cfg := &config.Config{
|
||||
ConfigPath: ds.originalConfig.ConfigPath,
|
||||
Queue: ds.originalConfig.Queue,
|
||||
System: ds.originalConfig.System,
|
||||
}
|
||||
|
||||
mainSet := &config.SetConfig{
|
||||
Id: ds.originalConfig.MainSet.Id,
|
||||
Name: ds.originalConfig.MainSet.Name,
|
||||
Enabled: true,
|
||||
TCP: preset.Config.TCP,
|
||||
UDP: preset.Config.UDP,
|
||||
Fragmentation: preset.Config.Fragmentation,
|
||||
Faking: preset.Config.Faking,
|
||||
Targets: config.TargetsConfig{
|
||||
SNIDomains: []string{testDomain},
|
||||
DomainsToMatch: []string{testDomain},
|
||||
IPs: []string{},
|
||||
IpsToMatch: []string{},
|
||||
GeoSiteCategories: []string{},
|
||||
GeoIpCategories: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
if mainSet.Faking.SNIMutation.Mode == "" {
|
||||
mainSet.Faking.SNIMutation.Mode = "off"
|
||||
}
|
||||
if mainSet.TCP.WinMode == "" {
|
||||
mainSet.TCP.WinMode = "off"
|
||||
}
|
||||
if mainSet.TCP.DesyncMode == "" {
|
||||
mainSet.TCP.DesyncMode = "off"
|
||||
}
|
||||
if mainSet.TCP.WinValues == nil {
|
||||
mainSet.TCP.WinValues = []int{0, 1460, 8192, 65535}
|
||||
}
|
||||
|
||||
cfg.MainSet = mainSet
|
||||
cfg.Sets = []*config.SetConfig{mainSet}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) logDiscoverySummary() {
|
||||
log.Infof("\n=== Discovery Results Summary ===")
|
||||
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
for _, domain := range ds.sortedDomains() {
|
||||
result := ds.domainResults[domain]
|
||||
if result.BestSuccess {
|
||||
log.Infof("✓ %s: %s (%.2f KB/s)",
|
||||
domain, result.BestPreset, result.BestSpeed/1024)
|
||||
} else {
|
||||
log.Warnf("✗ %s: No successful configuration found", domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) sortedDomains() []string {
|
||||
domains := make([]string, 0, len(ds.domainResults))
|
||||
for domain := range ds.domainResults {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
// GetDiscoveryReport returns formatted report
|
||||
func (ds *DiscoverySuite) GetDiscoveryReport() string {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
report := "Domain-Specific Configuration Discovery:\n"
|
||||
report += "=========================================\n\n"
|
||||
|
||||
for _, domain := range ds.sortedDomains() {
|
||||
result := ds.domainResults[domain]
|
||||
report += fmt.Sprintf("Domain: %s\n", domain)
|
||||
if result.BestSuccess {
|
||||
report += fmt.Sprintf(" Best Config: %s\n", result.BestPreset)
|
||||
report += fmt.Sprintf(" Speed: %.2f KB/s\n", result.BestSpeed/1024)
|
||||
} else {
|
||||
report += " Status: No successful configuration\n"
|
||||
}
|
||||
report += "\n"
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
|
@ -1,597 +0,0 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
)
|
||||
|
||||
type ConfigPreset struct {
|
||||
Name string
|
||||
Description string
|
||||
Config config.SetConfig
|
||||
}
|
||||
|
||||
// GetTestPresets generates focused preset combinations
|
||||
func GetTestPresets() []ConfigPreset {
|
||||
presets := []ConfigPreset{}
|
||||
|
||||
// 1. BASELINE - Always test first to compare
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "baseline",
|
||||
Description: "No bypass techniques",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 0, FakeLen: 0, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: false, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "none"},
|
||||
Faking: config.FakingConfig{SNI: false, TTL: 8, Strategy: "none", SeqOffset: 0, SNISeqLength: 0, SNIType: 2},
|
||||
},
|
||||
})
|
||||
|
||||
// 2. ConnBytesLimit Variations (CRITICAL - when bypass triggers)
|
||||
connBytesConfigs := []struct {
|
||||
tcp int
|
||||
udp int
|
||||
}{
|
||||
{1, 1}, // Immediate bypass
|
||||
{10, 5}, // Early bypass
|
||||
{19, 8}, // Default
|
||||
{50, 25}, // Late bypass
|
||||
{100, 50}, // Very late
|
||||
}
|
||||
|
||||
for _, cb := range connBytesConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("connbytes-tcp%d-udp%d", cb.tcp, cb.udp),
|
||||
Description: fmt.Sprintf("Trigger at TCP:%d UDP:%d bytes", cb.tcp, cb.udp),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: cb.tcp},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: cb.udp},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 3. SYN Fake with Combined Strategies
|
||||
synConfigs := []struct {
|
||||
synLen int
|
||||
strategy string
|
||||
ttl uint8
|
||||
}{
|
||||
{64, "ttl", 3},
|
||||
{64, "pastseq", 8},
|
||||
{256, "randseq", 5},
|
||||
{256, "tcp_check", 8},
|
||||
{512, "md5sum", 3},
|
||||
}
|
||||
|
||||
for _, sc := range synConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("syn-%d-%s", sc.synLen, sc.strategy),
|
||||
Description: fmt.Sprintf("SYN fake len=%d with %s", sc.synLen, sc.strategy),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, SynFake: true, SynFakeLen: sc.synLen},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: sc.ttl, Strategy: sc.strategy, SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 4. SNIType Variations (IMPORTANT - different payload types)
|
||||
sniTypeConfigs := []struct {
|
||||
sniType int
|
||||
payload string
|
||||
name string
|
||||
}{
|
||||
{0, "", "random"},
|
||||
{1, "GET / HTTP/1.1\r\nHost: ", "http"},
|
||||
{1, "\x00\x00\x00\x00\x00\x00\x00\x00", "nulls"},
|
||||
{1, "\xff\xff\xff\xff\xff\xff\xff\xff", "ones"},
|
||||
{1, "CONNECT ", "connect"},
|
||||
{2, "", "default"},
|
||||
}
|
||||
|
||||
for _, st := range sniTypeConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("snitype-%s", st.name),
|
||||
Description: fmt.Sprintf("SNI payload type: %s", st.name),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 2, SNIType: st.sniType, CustomPayload: st.payload},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 5. TCP Fragmentation with Extreme Positions
|
||||
fragConfigs := []struct {
|
||||
pos int
|
||||
reverse bool
|
||||
middle bool
|
||||
}{
|
||||
{1, false, false}, // Very early
|
||||
{1, true, false}, // Reverse
|
||||
{1, false, true}, // Middle SNI split
|
||||
{3, false, false}, // After version
|
||||
{11, false, false}, // After TLS header
|
||||
{50, false, false}, // Deep in handshake
|
||||
{100, true, false}, // Very deep + reverse
|
||||
}
|
||||
|
||||
for _, fc := range fragConfigs {
|
||||
name := fmt.Sprintf("tcp-pos%d", fc.pos)
|
||||
if fc.reverse {
|
||||
name += "-rev"
|
||||
}
|
||||
if fc.middle {
|
||||
name += "-mid"
|
||||
}
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Description: fmt.Sprintf("TCP frag pos=%d reverse=%v middle=%v", fc.pos, fc.reverse, fc.middle),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, Seg2Delay: 0},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: fc.pos, ReverseOrder: fc.reverse, MiddleSNI: fc.middle},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 6. OOB (Out-of-Band) Variations - NEW
|
||||
oobConfigs := []struct {
|
||||
pos int
|
||||
reverse bool
|
||||
char byte
|
||||
name string
|
||||
}{
|
||||
{1, false, 'x', "pos1"},
|
||||
{1, true, 'x', "pos1-rev"},
|
||||
{2, false, 'x', "pos2"},
|
||||
{3, false, 'x', "pos3"},
|
||||
{5, false, 'x', "pos5"},
|
||||
{1, false, 'a', "pos1-char-a"},
|
||||
{1, false, 0x00, "pos1-null"},
|
||||
{1, false, 0xFF, "pos1-xff"},
|
||||
{2, true, 'y', "pos2-rev-y"},
|
||||
{5, true, 'x', "pos5-rev"},
|
||||
}
|
||||
|
||||
for _, oc := range oobConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("oob-%s", oc.name),
|
||||
Description: fmt.Sprintf("OOB pos=%d reverse=%v char=%#x", oc.pos, oc.reverse, oc.char),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "oob", OOBPosition: oc.pos, ReverseOrder: oc.reverse, OOBChar: oc.char},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 7. OOB + Different Faking Strategies
|
||||
oobFakeConfigs := []struct {
|
||||
strategy string
|
||||
ttl uint8
|
||||
seqLen int
|
||||
pos int
|
||||
}{
|
||||
{"ttl", 3, 1, 1},
|
||||
{"ttl", 5, 2, 2},
|
||||
{"pastseq", 8, 2, 1},
|
||||
{"pastseq", 5, 3, 3},
|
||||
{"randseq", 8, 1, 1},
|
||||
{"tcp_check", 8, 2, 2},
|
||||
}
|
||||
|
||||
for _, ofc := range oobFakeConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("oob-fake-%s-pos%d", ofc.strategy, ofc.pos),
|
||||
Description: fmt.Sprintf("OOB pos=%d + fake %s seqLen=%d", ofc.pos, ofc.strategy, ofc.seqLen),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "oob", OOBPosition: ofc.pos, ReverseOrder: false, OOBChar: 'x'},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: ofc.ttl, Strategy: ofc.strategy, SeqOffset: 10000, SNISeqLength: ofc.seqLen, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 7a. SACK Dropping Variations
|
||||
sackConfigs := []struct {
|
||||
strategy string
|
||||
fragPos int
|
||||
reverse bool
|
||||
}{
|
||||
{"tcp", 1, false},
|
||||
{"tcp", 1, true},
|
||||
{"tcp", 3, false},
|
||||
{"ip", 1, false},
|
||||
{"oob", 1, false},
|
||||
{"oob", 2, true},
|
||||
}
|
||||
|
||||
for _, sc := range sackConfigs {
|
||||
name := fmt.Sprintf("sack-%s", sc.strategy)
|
||||
if sc.reverse {
|
||||
name += "-rev"
|
||||
}
|
||||
if sc.fragPos > 1 {
|
||||
name += fmt.Sprintf("-pos%d", sc.fragPos)
|
||||
}
|
||||
|
||||
fragConfig := config.FragmentationConfig{Strategy: sc.strategy, SNIPosition: sc.fragPos, ReverseOrder: sc.reverse}
|
||||
if sc.strategy == "oob" {
|
||||
fragConfig.OOBPosition = sc.fragPos
|
||||
fragConfig.OOBChar = 'x'
|
||||
}
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Description: fmt.Sprintf("SACK drop + %s frag pos=%d", sc.strategy, sc.fragPos),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, DropSACK: true},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: fragConfig,
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 7b. SACK + SYN Fake combinations
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "sack-syn-aggressive",
|
||||
Description: "SACK drop + SYN fake + TCP frag",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, DropSACK: true, SynFake: true, SynFakeLen: 256},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1, ReverseOrder: true},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 5, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 2, SNIType: 2},
|
||||
},
|
||||
})
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "sack-oob-ultra",
|
||||
Description: "SACK drop + OOB + immediate trigger",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 1, DropSACK: true},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 1},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "oob", OOBPosition: 1, ReverseOrder: true, OOBChar: 'x'},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 3, Strategy: "pastseq", SeqOffset: 50000, SNISeqLength: 3, SNIType: 2},
|
||||
},
|
||||
})
|
||||
|
||||
// 8. OOB + Early Triggering
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "oob-immediate",
|
||||
Description: "OOB with immediate trigger (connbytes=1)",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 1},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 1},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "oob", OOBPosition: 1, ReverseOrder: false, OOBChar: 'x'},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 2, SNIType: 2},
|
||||
},
|
||||
})
|
||||
|
||||
// 9. Faking Strategies with SeqOffset variations
|
||||
fakeConfigs := []struct {
|
||||
strategy string
|
||||
ttl uint8
|
||||
seqLen int
|
||||
offset int32
|
||||
}{
|
||||
{"ttl", 3, 1, 0},
|
||||
{"ttl", 5, 3, 0},
|
||||
{"ttl", 8, 5, 0},
|
||||
{"pastseq", 8, 1, 10000},
|
||||
{"pastseq", 5, 2, 50000},
|
||||
{"pastseq", 3, 3, 100000},
|
||||
{"randseq", 8, 1, 10000},
|
||||
{"randseq", 5, 2, 100000},
|
||||
{"md5sum", 8, 1, 0},
|
||||
{"tcp_check", 8, 2, 0},
|
||||
}
|
||||
|
||||
for _, fc := range fakeConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("fake-%s-ttl%d-len%d", fc.strategy, fc.ttl, fc.seqLen),
|
||||
Description: fmt.Sprintf("Fake %s TTL=%d seqLen=%d", fc.strategy, fc.ttl, fc.seqLen),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: fc.ttl, Strategy: fc.strategy, SeqOffset: fc.offset, SNISeqLength: fc.seqLen, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 10. UDP/QUIC with Port Filtering
|
||||
udpConfigs := []struct {
|
||||
mode string
|
||||
fakeLen int
|
||||
fakeSeq int
|
||||
strategy string
|
||||
quicFilter string
|
||||
dPort string
|
||||
}{
|
||||
{"fake", 64, 6, "ttl", "disabled", ""},
|
||||
{"fake", 128, 10, "checksum", "parse", ""},
|
||||
{"fake", 256, 12, "none", "all", ""},
|
||||
{"fake", 64, 8, "ttl", "parse", "443"},
|
||||
{"fake", 128, 10, "checksum", "all", "80,443"},
|
||||
{"drop", 0, 0, "none", "all", "443"},
|
||||
{"fake", 64, 6, "none", "disabled", ""},
|
||||
}
|
||||
|
||||
for _, uc := range udpConfigs {
|
||||
name := fmt.Sprintf("udp-%s", uc.mode)
|
||||
if uc.quicFilter != "" {
|
||||
name += fmt.Sprintf("-q%s", uc.quicFilter)
|
||||
}
|
||||
if uc.dPort != "" {
|
||||
name += fmt.Sprintf("-p%s", uc.dPort)
|
||||
}
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Description: fmt.Sprintf("UDP %s QUIC=%s ports=%s", uc.mode, uc.quicFilter, uc.dPort),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: uc.mode, FakeSeqLength: uc.fakeSeq, FakeLen: uc.fakeLen, FakingStrategy: uc.strategy, FilterQUIC: uc.quicFilter, FilterSTUN: uc.dPort == "", ConnBytesLimit: 8, DPortFilter: uc.dPort},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 11. IP Fragmentation variations
|
||||
ipFragConfigs := []struct {
|
||||
pos int
|
||||
reverse bool
|
||||
}{
|
||||
{1, false},
|
||||
{1, true},
|
||||
{8, false},
|
||||
{20, true},
|
||||
}
|
||||
|
||||
for _, ifc := range ipFragConfigs {
|
||||
name := fmt.Sprintf("ip-frag-pos%d", ifc.pos)
|
||||
if ifc.reverse {
|
||||
name += "-rev"
|
||||
}
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Description: fmt.Sprintf("IP frag at %d reverse=%v", ifc.pos, ifc.reverse),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "ip", SNIPosition: ifc.pos, ReverseOrder: ifc.reverse},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 11a. TLS Record Splitting Variations
|
||||
tlsConfigs := []struct {
|
||||
pos int
|
||||
reverse bool
|
||||
name string
|
||||
}{
|
||||
{1, false, "early"},
|
||||
{1, true, "early-rev"},
|
||||
{5, false, "mid"},
|
||||
{5, true, "mid-rev"},
|
||||
{10, false, "deep"},
|
||||
{20, false, "late"},
|
||||
{50, true, "late-rev"},
|
||||
{100, false, "extreme"},
|
||||
}
|
||||
|
||||
for _, tc := range tlsConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("tls-%s", tc.name),
|
||||
Description: fmt.Sprintf("TLS record split at %d bytes reverse=%v", tc.pos, tc.reverse),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: tc.pos, ReverseOrder: tc.reverse},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 11b. TLS + Faking Strategy combinations
|
||||
tlsFakeConfigs := []struct {
|
||||
tlsPos int
|
||||
strategy string
|
||||
ttl uint8
|
||||
seqLen int
|
||||
}{
|
||||
{1, "ttl", 3, 1},
|
||||
{5, "ttl", 5, 2},
|
||||
{1, "pastseq", 8, 2},
|
||||
{10, "pastseq", 5, 3},
|
||||
{1, "randseq", 8, 1},
|
||||
{5, "tcp_check", 8, 2},
|
||||
{20, "md5sum", 8, 1},
|
||||
}
|
||||
|
||||
for _, tfc := range tlsFakeConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("tls-pos%d-%s", tfc.tlsPos, tfc.strategy),
|
||||
Description: fmt.Sprintf("TLS pos=%d + fake %s TTL=%d", tfc.tlsPos, tfc.strategy, tfc.ttl),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: tfc.tlsPos, ReverseOrder: false},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: tfc.ttl, Strategy: tfc.strategy, SeqOffset: 10000, SNISeqLength: tfc.seqLen, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 11c. TLS + SYN Fake combinations
|
||||
tlsSynConfigs := []struct {
|
||||
tlsPos int
|
||||
synLen int
|
||||
ttl uint8
|
||||
}{
|
||||
{1, 64, 3},
|
||||
{5, 256, 5},
|
||||
{10, 512, 8},
|
||||
}
|
||||
|
||||
for _, tsc := range tlsSynConfigs {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("tls-syn-pos%d-len%d", tsc.tlsPos, tsc.synLen),
|
||||
Description: fmt.Sprintf("TLS pos=%d + SYN fake len=%d", tsc.tlsPos, tsc.synLen),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, SynFake: true, SynFakeLen: tsc.synLen},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: tsc.tlsPos, ReverseOrder: true},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: tsc.ttl, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 2, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 11d. TLS + SACK combinations
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "tls-sack-basic",
|
||||
Description: "TLS record split + SACK drop",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, DropSACK: true},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: 1, ReverseOrder: false},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 8, Strategy: "pastseq", SeqOffset: 10000, SNISeqLength: 1, SNIType: 2},
|
||||
},
|
||||
})
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "tls-sack-aggressive",
|
||||
Description: "TLS + SACK + SYN fake + immediate trigger",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 1, DropSACK: true, SynFake: true, SynFakeLen: 256},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 10, FakeLen: 128, FakingStrategy: "checksum", FilterQUIC: "all", FilterSTUN: true, ConnBytesLimit: 1},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: 5, ReverseOrder: true},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 3, Strategy: "randseq", SeqOffset: 100000, SNISeqLength: 3, SNIType: 0},
|
||||
},
|
||||
})
|
||||
|
||||
// 12. Delay variations
|
||||
delays := []int{5, 10, 20, 50}
|
||||
for _, delay := range delays {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("delay-%d", delay),
|
||||
Description: fmt.Sprintf("Segment delay %dms", delay),
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, Seg2Delay: delay},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8, Seg2Delay: delay},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1, ReverseOrder: true},
|
||||
Faking: config.FakingConfig{SNI: true, TTL: 5, Strategy: "randseq", SeqOffset: 50000, SNISeqLength: 3, SNIType: 2},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 13. Aggressive combinations
|
||||
aggressiveConfigs := []struct {
|
||||
name string
|
||||
synLen int
|
||||
udpSeq int
|
||||
ttl uint8
|
||||
strat string
|
||||
}{
|
||||
{"max-tcp", 256, 15, 3, "tcp"},
|
||||
{"max-oob", 512, 20, 1, "oob"},
|
||||
{"ultra-tcp", 512, 20, 1, "tcp"},
|
||||
{"ultra-oob", 512, 25, 1, "oob"},
|
||||
{"max-tls", 256, 15, 3, "tls"},
|
||||
{"max-tls-sack", 512, 20, 1, "tls"},
|
||||
{"ultra-tls", 512, 20, 1, "tls"},
|
||||
{"ultra-tls-sack", 512, 20, 1, "tls"},
|
||||
}
|
||||
|
||||
for _, ac := range aggressiveConfigs {
|
||||
var fragConfig config.FragmentationConfig
|
||||
var tcpConfig config.TCPConfig
|
||||
|
||||
switch ac.strat {
|
||||
case "oob":
|
||||
fragConfig = config.FragmentationConfig{Strategy: "oob", OOBPosition: 1, ReverseOrder: true, OOBChar: 'x'}
|
||||
tcpConfig = config.TCPConfig{ConnBytesLimit: 1, Seg2Delay: 10, SynFake: true, SynFakeLen: ac.synLen}
|
||||
case "tls":
|
||||
fragConfig = config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: 5, ReverseOrder: true}
|
||||
tcpConfig = config.TCPConfig{ConnBytesLimit: 1, Seg2Delay: 10, SynFake: true, SynFakeLen: ac.synLen}
|
||||
// Add SACK for "sack" variants
|
||||
if len(ac.name) > 4 && ac.name[len(ac.name)-4:] == "sack" {
|
||||
tcpConfig.DropSACK = true
|
||||
}
|
||||
default:
|
||||
fragConfig = config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1, ReverseOrder: true, MiddleSNI: true}
|
||||
tcpConfig = config.TCPConfig{ConnBytesLimit: 1, Seg2Delay: 10, SynFake: true, SynFakeLen: ac.synLen}
|
||||
}
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: fmt.Sprintf("aggressive-%s", ac.name),
|
||||
Description: fmt.Sprintf("%s bypass: all techniques", ac.name),
|
||||
Config: config.SetConfig{
|
||||
TCP: tcpConfig,
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: ac.udpSeq, FakeLen: 256, FakingStrategy: "checksum", FilterQUIC: "all", FilterSTUN: true, ConnBytesLimit: 1},
|
||||
Fragmentation: fragConfig,
|
||||
Faking: config.FakingConfig{SNI: true, TTL: ac.ttl, Strategy: "pastseq", SeqOffset: 100000, SNISeqLength: 5, SNIType: 0},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 14. Special edge cases
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "tls-only",
|
||||
Description: "TLS record split only, no fake SNI",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: 1, ReverseOrder: false},
|
||||
Faking: config.FakingConfig{SNI: false},
|
||||
},
|
||||
})
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "sack-only",
|
||||
Description: "SACK drop only, no fragmentation",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, DropSACK: true},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 0, FakeLen: 0, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "none"},
|
||||
Faking: config.FakingConfig{SNI: false},
|
||||
},
|
||||
})
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "tls-sack-only",
|
||||
Description: "TLS + SACK only, no fake SNI",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19, DropSACK: true},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 0, FakeLen: 0, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: 5, ReverseOrder: false},
|
||||
Faking: config.FakingConfig{SNI: false},
|
||||
},
|
||||
})
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "oob-only",
|
||||
Description: "OOB only, no fake SNI",
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{ConnBytesLimit: 19},
|
||||
UDP: config.UDPConfig{Mode: "fake", FakeSeqLength: 6, FakeLen: 64, FakingStrategy: "none", FilterQUIC: "disabled", FilterSTUN: true, ConnBytesLimit: 8},
|
||||
Fragmentation: config.FragmentationConfig{Strategy: "oob", OOBPosition: 1, ReverseOrder: false, OOBChar: 'x'},
|
||||
Faking: config.FakingConfig{SNI: false},
|
||||
},
|
||||
})
|
||||
|
||||
return presets
|
||||
}
|
||||
|
|
@ -131,12 +131,20 @@ var DefaultConfig = Config{
|
|||
},
|
||||
|
||||
Checker: CheckerConfig{
|
||||
TimeoutSeconds: 15,
|
||||
MaxConcurrent: 4,
|
||||
Domains: []string{},
|
||||
Domains: []string{},
|
||||
DiscoveryTimeoutSec: 5,
|
||||
ConfigPropagateMs: 1500,
|
||||
},
|
||||
API: ApiConfig{
|
||||
IPInfoToken: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func NewSetConfig() SetConfig {
|
||||
return DefaultSetConfig
|
||||
}
|
||||
|
||||
func NewConfig() Config {
|
||||
return DefaultConfig
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,9 +110,10 @@ type WebServerConfig struct {
|
|||
}
|
||||
|
||||
type CheckerConfig struct {
|
||||
TimeoutSeconds int `json:"timeout" bson:"timeout"`
|
||||
Domains []string `json:"domains" bson:"domains"`
|
||||
MaxConcurrent int `json:"max_concurrent" bson:"max_concurrent"`
|
||||
Domains []string `yaml:"domains" json:"domains"`
|
||||
// Discovery settings
|
||||
DiscoveryTimeoutSec int `yaml:"discovery_timeout" json:"discovery_timeout"`
|
||||
ConfigPropagateMs int `yaml:"config_propagate_ms" json:"config_propagate_ms"`
|
||||
}
|
||||
|
||||
type Logging struct {
|
||||
|
|
|
|||
250
src/discovery/cluster.go
Normal file
250
src/discovery/cluster.go
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
package discovery
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
func ClusterDomains(domains []string) []*DomainCluster {
|
||||
if len(domains) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Group by registrable domain (eTLD+1)
|
||||
groups := make(map[string][]string)
|
||||
ungrouped := []string{}
|
||||
|
||||
for _, domain := range domains {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get eTLD+1 (e.g., "youtube.com" from "www.youtube.com")
|
||||
etld, err := publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
if err != nil {
|
||||
ungrouped = append(ungrouped, domain)
|
||||
continue
|
||||
}
|
||||
|
||||
groups[etld] = append(groups[etld], domain)
|
||||
}
|
||||
|
||||
clusters := make([]*DomainCluster, 0, len(groups)+len(ungrouped))
|
||||
|
||||
// Create clusters for grouped domains
|
||||
for etld, domainList := range groups {
|
||||
// Sort to ensure consistent representative selection
|
||||
sort.Strings(domainList)
|
||||
|
||||
// Choose representative: prefer the shortest domain, or the eTLD itself
|
||||
representative := domainList[0]
|
||||
for _, d := range domainList {
|
||||
// Prefer exact eTLD match
|
||||
if d == etld {
|
||||
representative = d
|
||||
break
|
||||
}
|
||||
// Otherwise prefer shorter domains
|
||||
if len(d) < len(representative) {
|
||||
representative = d
|
||||
}
|
||||
}
|
||||
|
||||
clusters = append(clusters, &DomainCluster{
|
||||
ID: etld,
|
||||
Domains: domainList,
|
||||
Representative: representative,
|
||||
})
|
||||
}
|
||||
|
||||
// Add ungrouped domains as individual clusters
|
||||
for _, d := range ungrouped {
|
||||
clusters = append(clusters, &DomainCluster{
|
||||
ID: d,
|
||||
Domains: []string{d},
|
||||
Representative: d,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort clusters by number of domains (larger clusters first - more value to test)
|
||||
sort.Slice(clusters, func(i, j int) bool {
|
||||
return len(clusters[i].Domains) > len(clusters[j].Domains)
|
||||
})
|
||||
|
||||
return clusters
|
||||
}
|
||||
|
||||
// MergeClustersByIP groups clusters that resolve to the same IP ranges
|
||||
// This is useful for CDNs where different domains share infrastructure
|
||||
func MergeClustersByIP(clusters []*DomainCluster) []*DomainCluster {
|
||||
if len(clusters) <= 1 {
|
||||
return clusters
|
||||
}
|
||||
|
||||
// Resolve representative domains to IPs
|
||||
ipToCluster := make(map[string][]*DomainCluster)
|
||||
|
||||
for _, cluster := range clusters {
|
||||
ips, err := net.LookupIP(cluster.Representative)
|
||||
if err != nil || len(ips) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use first IP's /24 as key (rough grouping)
|
||||
ip := ips[0].To4()
|
||||
if ip == nil {
|
||||
ip = ips[0].To16()
|
||||
}
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create /24 key for IPv4, /64 for IPv6
|
||||
var key string
|
||||
if len(ip) == 4 {
|
||||
key = ip[:3].String() + ".0/24"
|
||||
} else {
|
||||
key = ip[:8].String() + "/64"
|
||||
}
|
||||
|
||||
ipToCluster[key] = append(ipToCluster[key], cluster)
|
||||
}
|
||||
|
||||
// Merge clusters that share IP ranges
|
||||
merged := make([]*DomainCluster, 0)
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, clusterGroup := range ipToCluster {
|
||||
if len(clusterGroup) <= 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Merge all clusters in this IP group
|
||||
mergedCluster := &DomainCluster{
|
||||
ID: clusterGroup[0].ID + "+merged",
|
||||
Domains: []string{},
|
||||
}
|
||||
|
||||
for _, c := range clusterGroup {
|
||||
mergedCluster.Domains = append(mergedCluster.Domains, c.Domains...)
|
||||
seen[c.ID] = true
|
||||
}
|
||||
|
||||
// Choose representative from merged cluster
|
||||
sort.Strings(mergedCluster.Domains)
|
||||
mergedCluster.Representative = mergedCluster.Domains[0]
|
||||
for _, d := range mergedCluster.Domains {
|
||||
if len(d) < len(mergedCluster.Representative) {
|
||||
mergedCluster.Representative = d
|
||||
}
|
||||
}
|
||||
|
||||
merged = append(merged, mergedCluster)
|
||||
}
|
||||
|
||||
// Add unmerged clusters
|
||||
for _, c := range clusters {
|
||||
if !seen[c.ID] {
|
||||
merged = append(merged, c)
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
// GetKnownCDNGroups returns domain patterns that belong to known CDNs
|
||||
// Domains matching these patterns can be grouped together
|
||||
var knownCDNPatterns = map[string][]string{
|
||||
"google": {
|
||||
"google.com", "googleapis.com", "gstatic.com", "googleusercontent.com",
|
||||
"googlevideo.com", "youtube.com", "ytimg.com", "ggpht.com",
|
||||
},
|
||||
"cloudflare": {
|
||||
"cloudflare.com", "cloudflare-dns.com", "cloudflareinsights.com",
|
||||
},
|
||||
"amazon": {
|
||||
"amazonaws.com", "cloudfront.net", "amazon.com", "aws.amazon.com",
|
||||
},
|
||||
"microsoft": {
|
||||
"microsoft.com", "msn.com", "live.com", "office.com", "azure.com",
|
||||
"windows.net", "microsoftonline.com",
|
||||
},
|
||||
"meta": {
|
||||
"facebook.com", "fbcdn.net", "instagram.com", "whatsapp.com", "whatsapp.net",
|
||||
},
|
||||
"twitter": {
|
||||
"twitter.com", "x.com", "twimg.com", "t.co",
|
||||
},
|
||||
}
|
||||
|
||||
// GetCDNGroup returns the CDN group name if domain belongs to a known CDN
|
||||
func GetCDNGroup(domain string) string {
|
||||
domain = strings.ToLower(domain)
|
||||
|
||||
for group, patterns := range knownCDNPatterns {
|
||||
for _, pattern := range patterns {
|
||||
if domain == pattern || strings.HasSuffix(domain, "."+pattern) {
|
||||
return group
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ClusterByKnownCDN groups domains by known CDN patterns before falling back to eTLD
|
||||
func ClusterByKnownCDN(domains []string) []*DomainCluster {
|
||||
cdnGroups := make(map[string][]string)
|
||||
remaining := []string{}
|
||||
|
||||
for _, domain := range domains {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if domain == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cdnGroup := GetCDNGroup(domain)
|
||||
if cdnGroup != "" {
|
||||
cdnGroups[cdnGroup] = append(cdnGroups[cdnGroup], domain)
|
||||
} else {
|
||||
remaining = append(remaining, domain)
|
||||
}
|
||||
}
|
||||
|
||||
clusters := make([]*DomainCluster, 0)
|
||||
|
||||
// Create clusters for CDN groups
|
||||
for cdnName, domainList := range cdnGroups {
|
||||
sort.Strings(domainList)
|
||||
|
||||
// Choose shortest domain as representative
|
||||
representative := domainList[0]
|
||||
for _, d := range domainList {
|
||||
if len(d) < len(representative) {
|
||||
representative = d
|
||||
}
|
||||
}
|
||||
|
||||
clusters = append(clusters, &DomainCluster{
|
||||
ID: "cdn:" + cdnName,
|
||||
Domains: domainList,
|
||||
Representative: representative,
|
||||
})
|
||||
}
|
||||
|
||||
// Cluster remaining domains by eTLD
|
||||
if len(remaining) > 0 {
|
||||
remainingClusters := ClusterDomains(remaining)
|
||||
clusters = append(clusters, remainingClusters...)
|
||||
}
|
||||
|
||||
// Sort by cluster size
|
||||
sort.Slice(clusters, func(i, j int) bool {
|
||||
return len(clusters[i].Domains) > len(clusters[j].Domains)
|
||||
})
|
||||
|
||||
return clusters
|
||||
}
|
||||
664
src/discovery/discovery.go
Normal file
664
src/discovery/discovery.go
Normal file
|
|
@ -0,0 +1,664 @@
|
|||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/daniellavrushin/b4/nfq"
|
||||
)
|
||||
|
||||
const (
|
||||
// Timeouts
|
||||
QUICK_FAIL_TIMEOUT = 1500 * time.Millisecond // Fast fail for non-responsive
|
||||
MAX_PRESETS_PER_DOMAIN = 3 // Stop after N successful configs
|
||||
|
||||
// Parallelism
|
||||
DEFAULT_PARALLEL_TESTS = 3
|
||||
)
|
||||
|
||||
type DiscoverySuite struct {
|
||||
*CheckSuite
|
||||
pool *nfq.Pool
|
||||
originalConfig *config.Config
|
||||
|
||||
// Hierarchical discovery state
|
||||
clusters []*DomainCluster
|
||||
workingFamilies map[string][]StrategyFamily // cluster -> working families
|
||||
bestParams map[string]map[StrategyFamily]ConfigPreset // cluster -> family -> best preset
|
||||
domainResults map[string]*DomainDiscoveryResult
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDiscoverySuite(checkConfig CheckConfig, pool *nfq.Pool) *DiscoverySuite {
|
||||
|
||||
suite := NewCheckSuite(checkConfig)
|
||||
suite.DomainDiscoveryResults = make(map[string]*DomainDiscoveryResult)
|
||||
|
||||
return &DiscoverySuite{
|
||||
CheckSuite: suite,
|
||||
pool: pool,
|
||||
workingFamilies: make(map[string][]StrategyFamily),
|
||||
bestParams: make(map[string]map[StrategyFamily]ConfigPreset),
|
||||
domainResults: make(map[string]*DomainDiscoveryResult),
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) RunDiscovery(domains []string) {
|
||||
// Register in activeSuites
|
||||
suitesMu.Lock()
|
||||
activeSuites[ds.Id] = ds.CheckSuite
|
||||
suitesMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
ds.EndTime = time.Now()
|
||||
time.AfterFunc(5*time.Minute, func() {
|
||||
suitesMu.Lock()
|
||||
delete(activeSuites, ds.Id)
|
||||
suitesMu.Unlock()
|
||||
})
|
||||
}()
|
||||
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.Status = CheckStatusRunning
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
// Store original configuration
|
||||
ds.originalConfig = ds.pool.GetFirstWorkerConfig()
|
||||
if ds.originalConfig == nil {
|
||||
log.Errorf("Failed to get original configuration")
|
||||
ds.setStatus(CheckStatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// Step 1: Cluster domains
|
||||
log.Infof("Clustering %d domains...", len(domains))
|
||||
ds.clusters = ClusterByKnownCDN(domains)
|
||||
log.Infof("Created %d clusters from %d domains", len(ds.clusters), len(domains))
|
||||
|
||||
for _, c := range ds.clusters {
|
||||
log.Tracef(" Cluster %s: %d domains, representative: %s", c.ID, len(c.Domains), c.Representative)
|
||||
}
|
||||
|
||||
// Initialize domain results for all domains
|
||||
for _, domain := range domains {
|
||||
ds.mu.Lock()
|
||||
ds.domainResults[domain] = &DomainDiscoveryResult{
|
||||
Domain: domain,
|
||||
Results: make(map[string]*DomainPresetResult),
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
}
|
||||
|
||||
// Set initial total as phase 1 only - will update as we discover working families
|
||||
phase1Presets := GetPhase1Presets()
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.TotalChecks = len(ds.clusters) * len(phase1Presets)
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
log.Infof("Starting hierarchical discovery for %d clusters", len(ds.clusters))
|
||||
log.Warnf("Service traffic will be affected during discovery testing")
|
||||
|
||||
// Step 2: For each cluster, run hierarchical discovery
|
||||
for _, cluster := range ds.clusters {
|
||||
select {
|
||||
case <-ds.cancel:
|
||||
log.Infof("Discovery suite %s canceled", ds.Id)
|
||||
ds.setStatus(CheckStatusCanceled)
|
||||
ds.restoreConfig()
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
ds.discoverCluster(cluster)
|
||||
}
|
||||
|
||||
// Step 3: Apply cluster results to all domains in each cluster
|
||||
ds.applyClusterResults()
|
||||
|
||||
// Restore original configuration
|
||||
ds.restoreConfig()
|
||||
|
||||
// Copy results to CheckSuite
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.CheckSuite.DomainDiscoveryResults = ds.domainResults
|
||||
ds.Status = CheckStatusComplete
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
ds.logDiscoverySummary()
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) discoverCluster(cluster *DomainCluster) {
|
||||
domain := cluster.Representative
|
||||
log.Infof("=== Discovering cluster %s (representative: %s, %d domains) ===",
|
||||
cluster.ID, domain, len(cluster.Domains))
|
||||
|
||||
// Phase 1: Strategy Detection
|
||||
ds.setPhase(PhaseStrategy)
|
||||
workingFamilies := ds.runPhase1(domain)
|
||||
|
||||
ds.mu.Lock()
|
||||
ds.workingFamilies[cluster.ID] = workingFamilies
|
||||
if ds.bestParams[cluster.ID] == nil {
|
||||
ds.bestParams[cluster.ID] = make(map[StrategyFamily]ConfigPreset)
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
|
||||
if len(workingFamilies) == 0 {
|
||||
log.Warnf("No working bypass strategies found for cluster %s", cluster.ID)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Phase 1 complete: %d working families: %v", len(workingFamilies), workingFamilies)
|
||||
|
||||
// Phase 2: Optimization (only for working families)
|
||||
ds.setPhase(PhaseOptimize)
|
||||
ds.runPhase2(domain, cluster.ID, workingFamilies)
|
||||
|
||||
// Phase 3: Combinations (if multiple families work)
|
||||
if len(workingFamilies) >= 2 {
|
||||
ds.setPhase(PhaseCombination)
|
||||
ds.runPhase3(domain, cluster.ID)
|
||||
}
|
||||
|
||||
// Mark cluster as tested and store best result
|
||||
cluster.Tested = true
|
||||
ds.determineBestForCluster(cluster)
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) runPhase1(domain string) []StrategyFamily {
|
||||
presets := GetPhase1Presets()
|
||||
workingFamilies := []StrategyFamily{}
|
||||
familyResults := make(map[StrategyFamily]*StrategyResult)
|
||||
|
||||
log.Infof("Phase 1: Testing %d strategy families", len(presets))
|
||||
|
||||
// Test baseline first (without any bypass)
|
||||
baselinePreset := presets[0]
|
||||
baselineResult := ds.testPreset(domain, baselinePreset)
|
||||
ds.storeResult(domain, baselinePreset, baselineResult)
|
||||
|
||||
baselineWorks := baselineResult.Status == CheckStatusComplete
|
||||
var baselineSpeed float64
|
||||
if baselineWorks {
|
||||
baselineSpeed = baselineResult.Speed
|
||||
log.Infof(" Baseline: SUCCESS (%.2f KB/s) - DPI bypass may not be needed", baselineSpeed/1024)
|
||||
|
||||
// Store baseline speed for improvement calculation
|
||||
ds.mu.Lock()
|
||||
if dr := ds.domainResults[domain]; dr != nil {
|
||||
dr.BaselineSpeed = baselineSpeed
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
} else {
|
||||
log.Infof(" Baseline: FAILED - DPI bypass needed")
|
||||
}
|
||||
|
||||
// Test each strategy family
|
||||
for _, preset := range presets[1:] { // Skip baseline
|
||||
select {
|
||||
case <-ds.cancel:
|
||||
return workingFamilies
|
||||
default:
|
||||
}
|
||||
|
||||
result := ds.testPreset(domain, preset)
|
||||
ds.storeResult(domain, preset, result)
|
||||
|
||||
sr := &StrategyResult{
|
||||
Family: preset.Family,
|
||||
Works: result.Status == CheckStatusComplete,
|
||||
Speed: result.Speed,
|
||||
Preset: preset.Name,
|
||||
Latency: result.Duration,
|
||||
}
|
||||
familyResults[preset.Family] = sr
|
||||
|
||||
if sr.Works {
|
||||
// Only count as "working" if it's better than baseline or baseline failed
|
||||
if !baselineWorks || sr.Speed > baselineSpeed*0.8 {
|
||||
workingFamilies = append(workingFamilies, preset.Family)
|
||||
log.Infof(" %s: SUCCESS (%.2f KB/s)", preset.Name, sr.Speed/1024)
|
||||
} else {
|
||||
log.Infof(" %s: SUCCESS but slower than baseline (%.2f vs %.2f KB/s)",
|
||||
preset.Name, sr.Speed/1024, baselineSpeed/1024)
|
||||
}
|
||||
} else {
|
||||
log.Tracef(" %s: FAILED (%s)", preset.Name, result.Error)
|
||||
}
|
||||
}
|
||||
|
||||
return workingFamilies
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) runPhase2(domain string, clusterID string, families []StrategyFamily) {
|
||||
// Calculate actual phase 2 preset count and update total
|
||||
totalPhase2Presets := 0
|
||||
for _, family := range families {
|
||||
totalPhase2Presets += len(GetPhase2Presets(family))
|
||||
}
|
||||
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.TotalChecks += totalPhase2Presets
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
log.Infof("Phase 2: Optimizing %d working families (%d presets)", len(families), totalPhase2Presets)
|
||||
|
||||
for _, family := range families {
|
||||
select {
|
||||
case <-ds.cancel:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
presets := GetPhase2Presets(family)
|
||||
if len(presets) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof(" Optimizing %s (%d variants)", family, len(presets))
|
||||
|
||||
var bestPreset ConfigPreset
|
||||
var bestSpeed float64
|
||||
successCount := 0
|
||||
|
||||
for _, preset := range presets {
|
||||
select {
|
||||
case <-ds.cancel:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Stop early if we found enough good configs
|
||||
if successCount >= 3 {
|
||||
log.Tracef(" Found %d good configs for %s, skipping rest", successCount, family)
|
||||
break
|
||||
}
|
||||
|
||||
result := ds.testPreset(domain, preset)
|
||||
ds.storeResult(domain, preset, result)
|
||||
|
||||
if result.Status == CheckStatusComplete {
|
||||
successCount++
|
||||
if result.Speed > bestSpeed {
|
||||
bestSpeed = result.Speed
|
||||
bestPreset = preset
|
||||
}
|
||||
log.Tracef(" %s: %.2f KB/s", preset.Name, result.Speed/1024)
|
||||
}
|
||||
}
|
||||
|
||||
if bestSpeed > 0 {
|
||||
ds.mu.Lock()
|
||||
ds.bestParams[clusterID][family] = bestPreset
|
||||
ds.mu.Unlock()
|
||||
log.Infof(" Best %s config: %s (%.2f KB/s)", family, bestPreset.Name, bestSpeed/1024)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) runPhase3(domain string, clusterID string) {
|
||||
ds.mu.RLock()
|
||||
workingFamilies := ds.workingFamilies[clusterID]
|
||||
bestParams := ds.bestParams[clusterID]
|
||||
ds.mu.RUnlock()
|
||||
|
||||
presets := GetCombinationPresets(workingFamilies, bestParams)
|
||||
if len(presets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Update total count
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.TotalChecks += len(presets)
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
log.Infof("Phase 3: Testing %d combination presets", len(presets))
|
||||
|
||||
for _, preset := range presets {
|
||||
select {
|
||||
case <-ds.cancel:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
result := ds.testPreset(domain, preset)
|
||||
ds.storeResult(domain, preset, result)
|
||||
|
||||
if result.Status == CheckStatusComplete {
|
||||
log.Infof(" %s: SUCCESS (%.2f KB/s)", preset.Name, result.Speed/1024)
|
||||
} else {
|
||||
log.Tracef(" %s: FAILED", preset.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) testPreset(domain string, preset ConfigPreset) CheckResult {
|
||||
// Build test config
|
||||
testConfig := ds.buildTestConfig(preset, domain)
|
||||
|
||||
// Apply config to pool
|
||||
if err := ds.pool.UpdateConfig(testConfig); err != nil {
|
||||
log.Errorf("Failed to apply preset %s: %v", preset.Name, err)
|
||||
return CheckResult{
|
||||
Domain: domain,
|
||||
Status: CheckStatusFailed,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Brief delay for config propagation
|
||||
time.Sleep(time.Duration(ds.Config.ConfigPropagateTimeout) * time.Millisecond)
|
||||
|
||||
// Test with quick fail first
|
||||
result := ds.quickTest(domain)
|
||||
|
||||
// If quick test failed but not timeout, try full test
|
||||
if result.Status == CheckStatusFailed && result.BytesRead == 0 {
|
||||
// Give it another shot with full timeout
|
||||
result = ds.fullTest(domain)
|
||||
}
|
||||
|
||||
result.Set = testConfig.MainSet
|
||||
|
||||
// Update progress
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.CompletedChecks++
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) quickTest(domain string) CheckResult {
|
||||
return ds.fetchWithTimeout(domain, QUICK_FAIL_TIMEOUT)
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) fullTest(domain string) CheckResult {
|
||||
return ds.fetchWithTimeout(domain, ds.Config.Timeout)
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) fetchWithTimeout(domain string, timeout time.Duration) CheckResult {
|
||||
result := CheckResult{
|
||||
Domain: domain,
|
||||
Status: CheckStatusRunning,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
testURL := fmt.Sprintf("https://%s/", domain)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
ResponseHeaderTimeout: timeout,
|
||||
IdleConnTimeout: timeout,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: timeout / 2,
|
||||
KeepAlive: timeout,
|
||||
}).DialContext,
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", testURL, nil)
|
||||
if err != nil {
|
||||
result.Status = CheckStatusFailed
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
result.Status = CheckStatusFailed
|
||||
result.Error = err.Error()
|
||||
result.Duration = time.Since(start)
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result.StatusCode = resp.StatusCode
|
||||
|
||||
// Read up to 100KB
|
||||
bytesRead, _ := io.CopyN(io.Discard, resp.Body, 100*1024)
|
||||
duration := time.Since(start)
|
||||
|
||||
result.Duration = duration
|
||||
result.BytesRead = bytesRead
|
||||
|
||||
if bytesRead > 0 {
|
||||
result.Status = CheckStatusComplete
|
||||
if duration.Seconds() > 0 {
|
||||
result.Speed = float64(bytesRead) / duration.Seconds()
|
||||
}
|
||||
} else {
|
||||
result.Status = CheckStatusFailed
|
||||
result.Error = "no data received"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) storeResult(domain string, preset ConfigPreset, result CheckResult) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
dr := ds.domainResults[domain]
|
||||
if dr == nil {
|
||||
dr = &DomainDiscoveryResult{
|
||||
Domain: domain,
|
||||
Results: make(map[string]*DomainPresetResult),
|
||||
}
|
||||
ds.domainResults[domain] = dr
|
||||
}
|
||||
|
||||
dr.Results[preset.Name] = &DomainPresetResult{
|
||||
PresetName: preset.Name,
|
||||
Family: preset.Family,
|
||||
Phase: preset.Phase,
|
||||
Status: result.Status,
|
||||
Duration: result.Duration,
|
||||
Speed: result.Speed,
|
||||
BytesRead: result.BytesRead,
|
||||
Error: result.Error,
|
||||
StatusCode: result.StatusCode,
|
||||
Set: result.Set,
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) determineBestForCluster(cluster *DomainCluster) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
dr := ds.domainResults[cluster.Representative]
|
||||
if dr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var bestPreset string
|
||||
var bestSpeed float64
|
||||
var bestSuccess bool
|
||||
|
||||
for presetName, result := range dr.Results {
|
||||
if result.Status == CheckStatusComplete {
|
||||
if !bestSuccess || result.Speed > bestSpeed {
|
||||
bestSuccess = true
|
||||
bestPreset = presetName
|
||||
bestSpeed = result.Speed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dr.BestPreset = bestPreset
|
||||
dr.BestSpeed = bestSpeed
|
||||
dr.BestSuccess = bestSuccess
|
||||
|
||||
// Calculate improvement over baseline
|
||||
if dr.BaselineSpeed > 0 && bestSpeed > 0 {
|
||||
dr.Improvement = ((bestSpeed - dr.BaselineSpeed) / dr.BaselineSpeed) * 100
|
||||
}
|
||||
|
||||
cluster.BestPreset = bestPreset
|
||||
cluster.BestSpeed = bestSpeed
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) applyClusterResults() {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
for _, cluster := range ds.clusters {
|
||||
if !cluster.Tested {
|
||||
continue
|
||||
}
|
||||
|
||||
repResult := ds.domainResults[cluster.Representative]
|
||||
if repResult == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply representative's results to all domains in cluster
|
||||
for _, domain := range cluster.Domains {
|
||||
if domain == cluster.Representative {
|
||||
continue
|
||||
}
|
||||
|
||||
dr := ds.domainResults[domain]
|
||||
if dr == nil {
|
||||
dr = &DomainDiscoveryResult{
|
||||
Domain: domain,
|
||||
Results: make(map[string]*DomainPresetResult),
|
||||
}
|
||||
ds.domainResults[domain] = dr
|
||||
}
|
||||
|
||||
// Copy best result from representative
|
||||
dr.BestPreset = repResult.BestPreset
|
||||
dr.BestSpeed = repResult.BestSpeed
|
||||
dr.BestSuccess = repResult.BestSuccess
|
||||
dr.WorkingFamilies = repResult.WorkingFamilies
|
||||
|
||||
// Copy the best preset's config
|
||||
if bestResult, ok := repResult.Results[repResult.BestPreset]; ok {
|
||||
dr.Results[repResult.BestPreset] = bestResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) buildTestConfig(preset ConfigPreset, testDomain string) *config.Config {
|
||||
mainSet := config.NewSetConfig()
|
||||
|
||||
mainSet.Id = ds.originalConfig.MainSet.Id
|
||||
mainSet.Name = preset.Name
|
||||
mainSet.TCP = preset.Config.TCP
|
||||
mainSet.UDP = preset.Config.UDP
|
||||
mainSet.Fragmentation = preset.Config.Fragmentation
|
||||
mainSet.Faking = preset.Config.Faking
|
||||
mainSet.Targets.SNIDomains = []string{testDomain}
|
||||
mainSet.Targets.DomainsToMatch = []string{testDomain}
|
||||
|
||||
return &config.Config{
|
||||
ConfigPath: ds.originalConfig.ConfigPath,
|
||||
Queue: ds.originalConfig.Queue,
|
||||
System: ds.originalConfig.System,
|
||||
MainSet: &mainSet,
|
||||
Sets: []*config.SetConfig{&mainSet},
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) setStatus(status CheckStatus) {
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.Status = status
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) setPhase(phase DiscoveryPhase) {
|
||||
ds.CheckSuite.mu.Lock()
|
||||
ds.CurrentPhase = phase
|
||||
ds.CheckSuite.mu.Unlock()
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) restoreConfig() {
|
||||
log.Infof("Restoring original configuration")
|
||||
if err := ds.pool.UpdateConfig(ds.originalConfig); err != nil {
|
||||
log.Errorf("Failed to restore original configuration: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DiscoverySuite) logDiscoverySummary() {
|
||||
log.Infof("\n=== Discovery Results Summary ===")
|
||||
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
// Sort domains for consistent output
|
||||
domains := make([]string, 0, len(ds.domainResults))
|
||||
for d := range ds.domainResults {
|
||||
domains = append(domains, d)
|
||||
}
|
||||
sort.Strings(domains)
|
||||
|
||||
successCount := 0
|
||||
for _, domain := range domains {
|
||||
result := ds.domainResults[domain]
|
||||
if result.BestSuccess {
|
||||
successCount++
|
||||
improvement := ""
|
||||
if result.Improvement > 0 {
|
||||
improvement = fmt.Sprintf(" (+%.0f%%)", result.Improvement)
|
||||
}
|
||||
log.Infof("✓ %s: %s (%.2f KB/s%s)",
|
||||
domain, result.BestPreset, result.BestSpeed/1024, improvement)
|
||||
} else {
|
||||
log.Warnf("✗ %s: No successful configuration found", domain)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("=== %d/%d domains with working configurations ===", successCount, len(domains))
|
||||
}
|
||||
|
||||
// GetDiscoveryReport returns formatted report
|
||||
func (ds *DiscoverySuite) GetDiscoveryReport() string {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
report := "Domain-Specific Configuration Discovery:\n"
|
||||
report += "=========================================\n\n"
|
||||
|
||||
domains := make([]string, 0, len(ds.domainResults))
|
||||
for d := range ds.domainResults {
|
||||
domains = append(domains, d)
|
||||
}
|
||||
sort.Strings(domains)
|
||||
|
||||
for _, domain := range domains {
|
||||
result := ds.domainResults[domain]
|
||||
report += fmt.Sprintf("Domain: %s\n", domain)
|
||||
if result.BestSuccess {
|
||||
report += fmt.Sprintf(" Best Config: %s\n", result.BestPreset)
|
||||
report += fmt.Sprintf(" Speed: %.2f KB/s\n", result.BestSpeed/1024)
|
||||
if result.Improvement > 0 {
|
||||
report += fmt.Sprintf(" Improvement: +%.0f%%\n", result.Improvement)
|
||||
}
|
||||
} else {
|
||||
report += " Status: No successful configuration\n"
|
||||
}
|
||||
report += "\n"
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
597
src/discovery/preset.go
Normal file
597
src/discovery/preset.go
Normal file
|
|
@ -0,0 +1,597 @@
|
|||
package discovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
)
|
||||
|
||||
// GetPhase1Presets returns minimal presets for strategy family detection
|
||||
// These are the "does this approach work at all?" tests
|
||||
// IMPORTANT: Most DPI requires COMBINATIONS of techniques, not single techniques
|
||||
func GetPhase1Presets() []ConfigPreset {
|
||||
return []ConfigPreset{
|
||||
// 0. Proven working config - this is the baseline that works for most Russian DPI
|
||||
{
|
||||
Name: "proven-combo",
|
||||
Description: "Proven combination: TCP frag + reverse + middle SNI + fake pastseq",
|
||||
Family: FamilyNone,
|
||||
Phase: PhaseBaseline,
|
||||
Priority: 0,
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
},
|
||||
UDP: config.UDPConfig{
|
||||
Mode: "fake",
|
||||
FakeSeqLength: 6,
|
||||
FakeLen: 64,
|
||||
FakingStrategy: "none",
|
||||
FilterQUIC: "disabled",
|
||||
FilterSTUN: true,
|
||||
ConnBytesLimit: 8,
|
||||
},
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "tcp",
|
||||
ReverseOrder: true,
|
||||
MiddleSNI: true,
|
||||
SNIPosition: 1,
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 8,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 1. Raw baseline - no bypass at all (to detect if DPI even blocks)
|
||||
{
|
||||
Name: "no-bypass",
|
||||
Description: "No bypass techniques - test raw connectivity",
|
||||
Family: FamilyNone,
|
||||
Phase: PhaseBaseline,
|
||||
Priority: 1,
|
||||
Config: baselineConfig(),
|
||||
},
|
||||
|
||||
// 2. TCP Frag + Fake (common combo)
|
||||
{
|
||||
Name: "tcp-frag-fake",
|
||||
Description: "TCP fragmentation with fake SNI",
|
||||
Family: FamilyTCPFrag,
|
||||
Phase: PhaseStrategy,
|
||||
Priority: 2,
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
},
|
||||
UDP: defaultUDP(),
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "tcp",
|
||||
SNIPosition: 1,
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 8,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 3. TCP Frag + Reverse + Fake
|
||||
{
|
||||
Name: "tcp-frag-rev-fake",
|
||||
Description: "TCP frag reverse order with fake SNI",
|
||||
Family: FamilyTCPFrag,
|
||||
Phase: PhaseStrategy,
|
||||
Priority: 3,
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
},
|
||||
UDP: defaultUDP(),
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "tcp",
|
||||
SNIPosition: 1,
|
||||
ReverseOrder: true,
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 8,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 4. TLS Record + Fake
|
||||
{
|
||||
Name: "tls-rec-fake",
|
||||
Description: "TLS record splitting with fake SNI",
|
||||
Family: FamilyTLSRec,
|
||||
Phase: PhaseStrategy,
|
||||
Priority: 4,
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
},
|
||||
UDP: defaultUDP(),
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "tls",
|
||||
TLSRecordPosition: 1,
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 8,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 5. OOB + Fake
|
||||
{
|
||||
Name: "oob-fake",
|
||||
Description: "Out-of-band with fake SNI",
|
||||
Family: FamilyOOB,
|
||||
Phase: PhaseStrategy,
|
||||
Priority: 5,
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
},
|
||||
UDP: defaultUDP(),
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "oob",
|
||||
OOBPosition: 1,
|
||||
OOBChar: 'x',
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 8,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 6. Fake only (low TTL)
|
||||
{
|
||||
Name: "fake-ttl-low",
|
||||
Description: "Fake SNI with low TTL (no fragmentation)",
|
||||
Family: FamilyFakeSNI,
|
||||
Phase: PhaseStrategy,
|
||||
Priority: 6,
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
},
|
||||
UDP: defaultUDP(),
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "none",
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 3,
|
||||
Strategy: "ttl",
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 7. SACK Drop + TCP Frag + Fake
|
||||
{
|
||||
Name: "sack-frag-fake",
|
||||
Description: "SACK drop with TCP frag and fake",
|
||||
Family: FamilySACK,
|
||||
Phase: PhaseStrategy,
|
||||
Priority: 7,
|
||||
Config: config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
DropSACK: true,
|
||||
},
|
||||
UDP: defaultUDP(),
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "tcp",
|
||||
SNIPosition: 1,
|
||||
ReverseOrder: true,
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 8,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultUDP() config.UDPConfig {
|
||||
return config.UDPConfig{
|
||||
Mode: "fake",
|
||||
FakeSeqLength: 6,
|
||||
FakeLen: 64,
|
||||
FakingStrategy: "none",
|
||||
FilterQUIC: "disabled",
|
||||
FilterSTUN: true,
|
||||
ConnBytesLimit: 8,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPhase2Presets generates optimization presets for a specific working family
|
||||
func GetPhase2Presets(family StrategyFamily) []ConfigPreset {
|
||||
base := baseConfig()
|
||||
presets := []ConfigPreset{}
|
||||
|
||||
switch family {
|
||||
case FamilyTCPFrag:
|
||||
positions := []int{1, 2, 3, 5, 10}
|
||||
for _, pos := range positions {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
name := formatName("tcp-pos%d", pos)
|
||||
if reverse {
|
||||
name += "-rev"
|
||||
}
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Family: FamilyTCPFrag,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: pos,
|
||||
Config: withFragmentation(base, config.FragmentationConfig{
|
||||
Strategy: "tcp",
|
||||
SNIPosition: pos,
|
||||
ReverseOrder: reverse,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
// Add middle SNI variant
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "tcp-middle-sni",
|
||||
Family: FamilyTCPFrag,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: 10,
|
||||
Config: withFragmentation(base, config.FragmentationConfig{
|
||||
Strategy: "tcp",
|
||||
SNIPosition: 1,
|
||||
MiddleSNI: true,
|
||||
}),
|
||||
})
|
||||
|
||||
case FamilyTLSRec:
|
||||
positions := []int{1, 5, 10, 20, 50}
|
||||
for _, pos := range positions {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
name := formatName("tls-pos%d", pos)
|
||||
if reverse {
|
||||
name += "-rev"
|
||||
}
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Family: FamilyTLSRec,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: pos,
|
||||
Config: withFragmentation(base, config.FragmentationConfig{
|
||||
Strategy: "tls",
|
||||
TLSRecordPosition: pos,
|
||||
ReverseOrder: reverse,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case FamilyOOB:
|
||||
positions := []int{1, 2, 3, 5}
|
||||
chars := []byte{'x', 'a', 0x00, 0xFF}
|
||||
for _, pos := range positions {
|
||||
for _, ch := range chars {
|
||||
name := formatName("oob-pos%d-0x%02x", pos, ch)
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Family: FamilyOOB,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: pos,
|
||||
Config: withFragmentation(base, config.FragmentationConfig{
|
||||
Strategy: "oob",
|
||||
OOBPosition: pos,
|
||||
OOBChar: ch,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case FamilyFakeSNI:
|
||||
// TTL variations
|
||||
ttls := []uint8{1, 2, 3, 5, 8}
|
||||
for _, ttl := range ttls {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: formatName("fake-ttl%d", ttl),
|
||||
Family: FamilyFakeSNI,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: int(ttl),
|
||||
Config: withFaking(base, config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: ttl,
|
||||
Strategy: "ttl",
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// Sequence length variations
|
||||
seqLens := []int{1, 2, 3, 5}
|
||||
for _, sl := range seqLens {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: formatName("fake-seq%d", sl),
|
||||
Family: FamilyFakeSNI,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: sl + 10,
|
||||
Config: withFaking(base, config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 3,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: sl,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// Strategy variations
|
||||
strategies := []string{"ttl", "pastseq", "randseq", "tcp_check", "md5sum"}
|
||||
for i, strat := range strategies {
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: formatName("fake-%s", strat),
|
||||
Family: FamilyFakeSNI,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: i + 20,
|
||||
Config: withFaking(base, config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 3,
|
||||
Strategy: strat,
|
||||
SeqOffset: 10000,
|
||||
SNISeqLength: 1,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
case FamilyIPFrag:
|
||||
positions := []int{1, 8, 16, 24}
|
||||
for _, pos := range positions {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
name := formatName("ip-pos%d", pos)
|
||||
if reverse {
|
||||
name += "-rev"
|
||||
}
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: name,
|
||||
Family: FamilyIPFrag,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: pos,
|
||||
Config: withFragmentation(base, config.FragmentationConfig{
|
||||
Strategy: "ip",
|
||||
SNIPosition: pos,
|
||||
ReverseOrder: reverse,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case FamilySACK:
|
||||
// SACK + different fragmentation strategies
|
||||
fragStrategies := []string{"tcp", "tls", "oob"}
|
||||
for i, fs := range fragStrategies {
|
||||
cfg := withTCP(base, config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
DropSACK: true,
|
||||
})
|
||||
switch fs {
|
||||
case "tcp":
|
||||
cfg = withFragmentation(cfg, config.FragmentationConfig{Strategy: "tcp", SNIPosition: 1})
|
||||
case "tls":
|
||||
cfg = withFragmentation(cfg, config.FragmentationConfig{Strategy: "tls", TLSRecordPosition: 1})
|
||||
case "oob":
|
||||
cfg = withFragmentation(cfg, config.FragmentationConfig{Strategy: "oob", OOBPosition: 1, OOBChar: 'x'})
|
||||
}
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: formatName("sack-%s", fs),
|
||||
Family: FamilySACK,
|
||||
Phase: PhaseOptimize,
|
||||
Priority: i,
|
||||
Config: cfg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return presets
|
||||
}
|
||||
|
||||
// GetCombinationPresets generates presets combining multiple working families
|
||||
func GetCombinationPresets(workingFamilies []StrategyFamily, bestParams map[StrategyFamily]ConfigPreset) []ConfigPreset {
|
||||
presets := []ConfigPreset{}
|
||||
|
||||
// If we have both fragmentation and faking working, combine them
|
||||
hasFrag := containsFamily(workingFamilies, FamilyTCPFrag) || containsFamily(workingFamilies, FamilyTLSRec) || containsFamily(workingFamilies, FamilyOOB)
|
||||
hasFake := containsFamily(workingFamilies, FamilyFakeSNI)
|
||||
hasSACK := containsFamily(workingFamilies, FamilySACK)
|
||||
|
||||
base := baseConfig()
|
||||
|
||||
if hasFrag && hasFake {
|
||||
// Combine best frag with best fake
|
||||
var fragConfig config.FragmentationConfig
|
||||
var fakingConfig config.FakingConfig
|
||||
|
||||
// Get best fragmentation params
|
||||
for _, fam := range []StrategyFamily{FamilyTCPFrag, FamilyTLSRec, FamilyOOB} {
|
||||
if bp, ok := bestParams[fam]; ok {
|
||||
fragConfig = bp.Config.Fragmentation
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get best faking params
|
||||
if bp, ok := bestParams[FamilyFakeSNI]; ok {
|
||||
fakingConfig = bp.Config.Faking
|
||||
}
|
||||
|
||||
combined := withFragmentation(base, fragConfig)
|
||||
combined = withFaking(combined, fakingConfig)
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "combo-frag-fake",
|
||||
Description: "Combined fragmentation + fake SNI",
|
||||
Family: FamilyNone,
|
||||
Phase: PhaseCombination,
|
||||
Priority: 1,
|
||||
Config: combined,
|
||||
})
|
||||
}
|
||||
|
||||
if hasSACK && hasFrag {
|
||||
// SACK + fragmentation
|
||||
var fragConfig config.FragmentationConfig
|
||||
for _, fam := range []StrategyFamily{FamilyTCPFrag, FamilyTLSRec, FamilyOOB} {
|
||||
if bp, ok := bestParams[fam]; ok {
|
||||
fragConfig = bp.Config.Fragmentation
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
combined := withTCP(base, config.TCPConfig{ConnBytesLimit: 19, DropSACK: true})
|
||||
combined = withFragmentation(combined, fragConfig)
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "combo-sack-frag",
|
||||
Description: "SACK drop + fragmentation",
|
||||
Family: FamilyNone,
|
||||
Phase: PhaseCombination,
|
||||
Priority: 2,
|
||||
Config: combined,
|
||||
})
|
||||
}
|
||||
|
||||
// Aggressive combo - everything together
|
||||
if len(workingFamilies) >= 2 {
|
||||
aggressive := config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 1,
|
||||
Seg2Delay: 5,
|
||||
DropSACK: hasSACK,
|
||||
SynFake: true,
|
||||
SynFakeLen: 256,
|
||||
},
|
||||
UDP: config.UDPConfig{
|
||||
Mode: "fake",
|
||||
FakeSeqLength: 10,
|
||||
FakeLen: 128,
|
||||
FakingStrategy: "checksum",
|
||||
FilterQUIC: "all",
|
||||
FilterSTUN: true,
|
||||
ConnBytesLimit: 1,
|
||||
},
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "tcp",
|
||||
SNIPosition: 1,
|
||||
ReverseOrder: true,
|
||||
MiddleSNI: true,
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: true,
|
||||
TTL: 3,
|
||||
Strategy: "pastseq",
|
||||
SeqOffset: 50000,
|
||||
SNISeqLength: 3,
|
||||
SNIType: config.FakePayloadDefault,
|
||||
},
|
||||
}
|
||||
|
||||
presets = append(presets, ConfigPreset{
|
||||
Name: "aggressive",
|
||||
Description: "All bypass techniques combined",
|
||||
Family: FamilyNone,
|
||||
Phase: PhaseCombination,
|
||||
Priority: 10,
|
||||
Config: aggressive,
|
||||
})
|
||||
}
|
||||
|
||||
return presets
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func baseConfig() config.SetConfig {
|
||||
return config.NewSetConfig()
|
||||
}
|
||||
|
||||
func baselineConfig() config.SetConfig {
|
||||
return config.SetConfig{
|
||||
TCP: config.TCPConfig{
|
||||
ConnBytesLimit: 19,
|
||||
},
|
||||
UDP: config.UDPConfig{
|
||||
Mode: "fake",
|
||||
FakeSeqLength: 0,
|
||||
FakeLen: 0,
|
||||
FakingStrategy: "none",
|
||||
FilterQUIC: "disabled",
|
||||
FilterSTUN: false,
|
||||
ConnBytesLimit: 8,
|
||||
},
|
||||
Fragmentation: config.FragmentationConfig{
|
||||
Strategy: "none",
|
||||
},
|
||||
Faking: config.FakingConfig{
|
||||
SNI: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func withFragmentation(base config.SetConfig, frag config.FragmentationConfig) config.SetConfig {
|
||||
base.Fragmentation = frag
|
||||
return base
|
||||
}
|
||||
|
||||
func withFaking(base config.SetConfig, faking config.FakingConfig) config.SetConfig {
|
||||
base.Faking = faking
|
||||
return base
|
||||
}
|
||||
|
||||
func withTCP(base config.SetConfig, tcp config.TCPConfig) config.SetConfig {
|
||||
base.TCP = tcp
|
||||
return base
|
||||
}
|
||||
|
||||
func formatName(format string, args ...interface{}) string {
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
func containsFamily(families []StrategyFamily, target StrategyFamily) bool {
|
||||
for _, f := range families {
|
||||
if f == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package checker
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -273,6 +273,8 @@ func (ts *CheckSuite) GetSnapshot() *CheckSuite {
|
|||
Config: ts.Config,
|
||||
PresetResults: ts.PresetResults,
|
||||
DomainDiscoveryResults: ts.DomainDiscoveryResults,
|
||||
CurrentPhase: ts.CurrentPhase,
|
||||
WorkingFamilies: ts.WorkingFamilies,
|
||||
}
|
||||
|
||||
snapshot.Results = make([]CheckResult, len(ts.Results))
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package checker
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
|
@ -17,6 +17,30 @@ const (
|
|||
CheckStatusCanceled CheckStatus = "canceled"
|
||||
)
|
||||
|
||||
// DiscoveryPhase represents the current phase of hierarchical discovery
|
||||
type DiscoveryPhase string
|
||||
|
||||
const (
|
||||
PhaseBaseline DiscoveryPhase = "baseline"
|
||||
PhaseStrategy DiscoveryPhase = "strategy_detection"
|
||||
PhaseOptimize DiscoveryPhase = "optimization"
|
||||
PhaseCombination DiscoveryPhase = "combination"
|
||||
)
|
||||
|
||||
// StrategyFamily groups related bypass techniques
|
||||
type StrategyFamily string
|
||||
|
||||
const (
|
||||
FamilyNone StrategyFamily = "none"
|
||||
FamilyTCPFrag StrategyFamily = "tcp_frag"
|
||||
FamilyTLSRec StrategyFamily = "tls_record"
|
||||
FamilyOOB StrategyFamily = "oob"
|
||||
FamilyIPFrag StrategyFamily = "ip_frag"
|
||||
FamilyFakeSNI StrategyFamily = "fake_sni"
|
||||
FamilySACK StrategyFamily = "sack"
|
||||
FamilySynFake StrategyFamily = "syn_fake"
|
||||
)
|
||||
|
||||
type CheckResult struct {
|
||||
Domain string `json:"domain"`
|
||||
Category string `json:"category"`
|
||||
|
|
@ -48,6 +72,10 @@ type CheckSuite struct {
|
|||
mu sync.RWMutex `json:"-"`
|
||||
cancel chan struct{} `json:"-"`
|
||||
Config CheckConfig `json:"config"`
|
||||
|
||||
// Hierarchical discovery fields
|
||||
CurrentPhase DiscoveryPhase `json:"current_phase,omitempty"`
|
||||
WorkingFamilies []string `json:"working_families,omitempty"`
|
||||
}
|
||||
|
||||
type CheckSummary struct {
|
||||
|
|
@ -59,10 +87,11 @@ type CheckSummary struct {
|
|||
}
|
||||
|
||||
type CheckConfig struct {
|
||||
CheckURL string `json:"check_url"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
SamplesPerDomain int `json:"samples_per_domain"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
CheckURL string `json:"check_url"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
ConfigPropagateTimeout time.Duration `json:"config_propagate_timeout"`
|
||||
SamplesPerDomain int `json:"samples_per_domain"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
}
|
||||
|
||||
type DomainSample struct {
|
||||
|
|
@ -79,6 +108,8 @@ type ConfigTestMode struct {
|
|||
|
||||
type DomainPresetResult struct {
|
||||
PresetName string `json:"preset_name"`
|
||||
Family StrategyFamily `json:"family,omitempty"`
|
||||
Phase DiscoveryPhase `json:"phase,omitempty"`
|
||||
Status CheckStatus `json:"status"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Speed float64 `json:"speed"`
|
||||
|
|
@ -89,9 +120,42 @@ type DomainPresetResult struct {
|
|||
}
|
||||
|
||||
type DomainDiscoveryResult struct {
|
||||
Domain string `json:"domain"`
|
||||
BestPreset string `json:"best_preset"`
|
||||
BestSpeed float64 `json:"best_speed"`
|
||||
BestSuccess bool `json:"best_success"`
|
||||
Results map[string]*DomainPresetResult `json:"results"`
|
||||
Domain string `json:"domain"`
|
||||
BestPreset string `json:"best_preset"`
|
||||
BestSpeed float64 `json:"best_speed"`
|
||||
BestSuccess bool `json:"best_success"`
|
||||
Results map[string]*DomainPresetResult `json:"results"`
|
||||
WorkingFamilies []StrategyFamily `json:"working_families,omitempty"`
|
||||
BaselineSpeed float64 `json:"baseline_speed,omitempty"`
|
||||
Improvement float64 `json:"improvement,omitempty"`
|
||||
}
|
||||
|
||||
// DomainCluster groups domains that likely need the same bypass config
|
||||
type DomainCluster struct {
|
||||
ID string `json:"id"`
|
||||
Domains []string `json:"domains"`
|
||||
Representative string `json:"representative"` // Domain we actually test
|
||||
BestPreset string `json:"best_preset,omitempty"`
|
||||
BestSpeed float64 `json:"best_speed,omitempty"`
|
||||
Tested bool `json:"tested"`
|
||||
}
|
||||
|
||||
// ConfigPreset represents a bypass configuration to test
|
||||
type ConfigPreset struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Family StrategyFamily `json:"family"`
|
||||
Phase DiscoveryPhase `json:"phase"`
|
||||
Config config.SetConfig `json:"config"`
|
||||
Priority int `json:"priority"` // Lower = test first
|
||||
ConfigPropagateTimeout int `json:"propagate_timeout"`
|
||||
}
|
||||
|
||||
// StrategyResult tracks whether a strategy family works
|
||||
type StrategyResult struct {
|
||||
Family StrategyFamily
|
||||
Works bool
|
||||
Speed float64
|
||||
Preset string
|
||||
Latency time.Duration
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ require (
|
|||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
)
|
||||
|
||||
|
|
@ -22,6 +21,7 @@ require (
|
|||
github.com/urlesistiana/v2dat v0.0.0-20221215035016-47b8ee51fb52
|
||||
github.com/yl2chen/cidranger v1.0.2
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/sys v0.37.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ func (api *API) RegisterEndpoints(mux *http.ServeMux, cfg *config.Config) {
|
|||
api.RegisterGeositeApi()
|
||||
api.RegisterGeoipApi()
|
||||
api.RegisterSystemApi()
|
||||
api.RegisterCheckApi()
|
||||
api.RegisterDiscoveryApi()
|
||||
api.RegisterIntegrationApi()
|
||||
api.RegisterGeodatApi()
|
||||
api.RegisterCaptureApi()
|
||||
|
|
|
|||
|
|
@ -6,19 +6,19 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/daniellavrushin/b4/checker"
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/discovery"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/daniellavrushin/b4/utils"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (api *API) RegisterCheckApi() {
|
||||
api.mux.HandleFunc("/api/check/start", api.handleStartCheck)
|
||||
api.mux.HandleFunc("/api/check/discovery", api.handleStartDiscovery)
|
||||
api.mux.HandleFunc("/api/check/status", api.handleCheckStatus)
|
||||
api.mux.HandleFunc("/api/check/cancel", api.handleCancelCheck)
|
||||
api.mux.HandleFunc("/api/check/add", api.handleAddPresetAsSet)
|
||||
func (api *API) RegisterDiscoveryApi() {
|
||||
api.mux.HandleFunc("/api/discovery", api.handleStartDiscovery)
|
||||
api.mux.HandleFunc("/api/discovery/start", api.handleStartCheck)
|
||||
api.mux.HandleFunc("/api/discovery/status", api.handleCheckStatus)
|
||||
api.mux.HandleFunc("/api/discovery/cancel", api.handleCancelCheck)
|
||||
api.mux.HandleFunc("/api/discovery/add", api.handleAddPresetAsSet)
|
||||
}
|
||||
|
||||
func (api *API) handleStartCheck(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -35,13 +35,6 @@ func (api *API) handleStartCheck(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if req.Timeout <= 0 {
|
||||
req.Timeout = time.Duration(chckCfg.TimeoutSeconds) * time.Second
|
||||
}
|
||||
if req.MaxConcurrent <= 0 {
|
||||
req.MaxConcurrent = chckCfg.MaxConcurrent
|
||||
}
|
||||
|
||||
domains := req.Domains
|
||||
if len(domains) == 0 {
|
||||
if len(api.cfg.Sets) > 0 {
|
||||
|
|
@ -59,13 +52,13 @@ func (api *API) handleStartCheck(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "No domains provided. Please specify domains to test.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
config := checker.CheckConfig{
|
||||
CheckURL: req.CheckURL,
|
||||
Timeout: req.Timeout,
|
||||
MaxConcurrent: req.MaxConcurrent,
|
||||
config := discovery.CheckConfig{
|
||||
CheckURL: req.CheckURL,
|
||||
Timeout: time.Duration(api.cfg.System.Checker.DiscoveryTimeoutSec) * time.Second,
|
||||
ConfigPropagateTimeout: time.Duration(api.cfg.System.Checker.ConfigPropagateMs),
|
||||
}
|
||||
|
||||
suite := checker.NewCheckSuite(config)
|
||||
suite := discovery.NewCheckSuite(config)
|
||||
|
||||
go suite.Run(domains)
|
||||
|
||||
|
|
@ -92,7 +85,7 @@ func (api *API) handleCheckStatus(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
suite, ok := checker.GetCheckSuite(testID)
|
||||
suite, ok := discovery.GetCheckSuite(testID)
|
||||
if !ok {
|
||||
http.Error(w, "Check suite not found", http.StatusNotFound)
|
||||
return
|
||||
|
|
@ -116,7 +109,7 @@ func (api *API) handleCancelCheck(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := checker.CancelCheckSuite(testID); err != nil {
|
||||
if err := discovery.CancelCheckSuite(testID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
|
@ -145,13 +138,6 @@ func (api *API) handleStartDiscovery(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if req.Timeout <= 0 {
|
||||
req.Timeout = time.Duration(chckCfg.TimeoutSeconds) * time.Second
|
||||
}
|
||||
if req.MaxConcurrent <= 0 {
|
||||
req.MaxConcurrent = chckCfg.MaxConcurrent
|
||||
}
|
||||
|
||||
domains := req.Domains
|
||||
if len(domains) == 0 {
|
||||
if len(api.cfg.Sets) > 0 {
|
||||
|
|
@ -169,27 +155,35 @@ func (api *API) handleStartDiscovery(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "No domains provided. Please specify domains to test.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
config := checker.CheckConfig{
|
||||
CheckURL: req.CheckURL,
|
||||
Timeout: req.Timeout,
|
||||
MaxConcurrent: req.MaxConcurrent,
|
||||
|
||||
config := discovery.CheckConfig{
|
||||
CheckURL: req.CheckURL,
|
||||
Timeout: time.Duration(api.cfg.System.Checker.DiscoveryTimeoutSec) * time.Second,
|
||||
ConfigPropagateTimeout: time.Duration(api.cfg.System.Checker.ConfigPropagateMs),
|
||||
}
|
||||
|
||||
presets := checker.GetTestPresets()
|
||||
// Pass geodata manager for geosite-based clustering
|
||||
suite := discovery.NewDiscoverySuite(config, globalPool)
|
||||
|
||||
suite := checker.NewDiscoverySuite(config, globalPool, presets)
|
||||
clusters := discovery.ClusterByKnownCDN(domains)
|
||||
|
||||
phase1Count := len(discovery.GetPhase1Presets())
|
||||
estimatedTests := len(clusters) * (phase1Count + 10)
|
||||
|
||||
go func() {
|
||||
suite.RunDiscovery(domains)
|
||||
|
||||
log.Infof("Discovery complete for %d domains", len(domains))
|
||||
log.Infof("Discovery complete for %d domains (%d clusters)", len(domains), len(clusters))
|
||||
log.Infof("\n%s", suite.GetDiscoveryReport())
|
||||
}()
|
||||
|
||||
response := StartCheckResponse{
|
||||
Id: suite.Id,
|
||||
TotalChecks: len(domains) * len(presets),
|
||||
Message: fmt.Sprintf("Discovery started: %d domains × %d presets", len(domains), len(presets)),
|
||||
response := DiscoveryStartResponse{
|
||||
Id: suite.Id,
|
||||
TotalDomains: len(domains),
|
||||
TotalClusters: len(clusters),
|
||||
EstimatedTests: estimatedTests,
|
||||
Message: fmt.Sprintf("Hierarchical discovery started: %d domains in %d clusters (~%d tests)",
|
||||
len(domains), len(clusters), estimatedTests),
|
||||
}
|
||||
|
||||
setJsonHeader(w)
|
||||
|
|
@ -222,6 +216,39 @@ func (api *API) handleAddPresetAsSet(w http.ResponseWriter, r *http.Request) {
|
|||
set.Name = set.Targets.SNIDomains[0]
|
||||
set.Targets.DomainsToMatch = []string{set.Targets.SNIDomains[0]}
|
||||
|
||||
// Ensure all target arrays are initialized (not null)
|
||||
if set.Targets.IPs == nil {
|
||||
set.Targets.IPs = []string{}
|
||||
}
|
||||
if set.Targets.IpsToMatch == nil {
|
||||
set.Targets.IpsToMatch = []string{}
|
||||
}
|
||||
if set.Targets.GeoSiteCategories == nil {
|
||||
set.Targets.GeoSiteCategories = []string{}
|
||||
}
|
||||
if set.Targets.GeoIpCategories == nil {
|
||||
set.Targets.GeoIpCategories = []string{}
|
||||
}
|
||||
|
||||
// Ensure TCP WinValues is initialized
|
||||
if set.TCP.WinValues == nil {
|
||||
set.TCP.WinValues = []int{0, 1460, 8192, 65535}
|
||||
}
|
||||
if set.TCP.WinMode == "" {
|
||||
set.TCP.WinMode = "off"
|
||||
}
|
||||
if set.TCP.DesyncMode == "" {
|
||||
set.TCP.DesyncMode = "off"
|
||||
}
|
||||
|
||||
// Ensure Faking SNIMutation is initialized
|
||||
if set.Faking.SNIMutation.Mode == "" {
|
||||
set.Faking.SNIMutation.Mode = "off"
|
||||
}
|
||||
if set.Faking.SNIMutation.FakeSNIs == nil {
|
||||
set.Faking.SNIMutation.FakeSNIs = []string{}
|
||||
}
|
||||
|
||||
api.cfg.Sets = append([]*config.SetConfig{&set}, api.cfg.Sets...)
|
||||
|
||||
if api.cfg.MainSet == nil {
|
||||
|
|
@ -242,3 +269,12 @@ func (api *API) handleAddPresetAsSet(w http.ResponseWriter, r *http.Request) {
|
|||
"message": fmt.Sprintf("Added '%s' configuration", set.Name),
|
||||
})
|
||||
}
|
||||
|
||||
// DiscoveryStartResponse includes cluster information
|
||||
type DiscoveryStartResponse struct {
|
||||
Id string `json:"id"`
|
||||
TotalDomains int `json:"total_domains"`
|
||||
TotalClusters int `json:"total_clusters"`
|
||||
EstimatedTests int `json:"estimated_tests"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
package handler
|
||||
|
||||
import "time"
|
||||
|
||||
type StartCheckRequest struct {
|
||||
CheckURL string `json:"check_url,omitempty"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
CheckURL string `json:"check_url,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
type StartCheckResponse struct {
|
||||
|
|
@ -23,21 +23,24 @@ import {
|
|||
Divider,
|
||||
Badge,
|
||||
} from "@mui/material";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import LanguageIcon from "@mui/icons-material/Language";
|
||||
import SpeedIcon from "@mui/icons-material/Speed";
|
||||
import AssessmentIcon from "@mui/icons-material/Assessment";
|
||||
import ScienceIcon from "@mui/icons-material/Science";
|
||||
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Settings as SettingsIcon,
|
||||
Language as LanguageIcon,
|
||||
Speed as SpeedIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Science as ScienceIcon,
|
||||
} from "@mui/icons-material";
|
||||
import Dashboard from "@pages/Dashboard";
|
||||
import Logs from "@pages/Logs";
|
||||
import Domains from "@pages/Domains";
|
||||
import Settings from "@pages/Settings";
|
||||
import Test from "@pages/Checker";
|
||||
import { theme, colors } from "@design";
|
||||
import Logo from "@molecules/Logo";
|
||||
import Version from "@organisms/version/Version";
|
||||
import { useWebSocket } from "@ctx/B4WsProvider";
|
||||
import Discovery from "@pages/Discovery";
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
|
||||
|
|
@ -50,7 +53,7 @@ interface NavItem {
|
|||
const navItems: NavItem[] = [
|
||||
{ path: "/dashboard", label: "Dashboard", icon: <SpeedIcon /> },
|
||||
{ path: "/domains", label: "Domains", icon: <LanguageIcon /> },
|
||||
{ path: "/test", label: "Test", icon: <ScienceIcon /> },
|
||||
{ path: "/discovery", label: "Discovery", icon: <ScienceIcon /> },
|
||||
{ path: "/logs", label: "Logs", icon: <AssessmentIcon /> },
|
||||
{ path: "/settings", label: "Settings", icon: <SettingsIcon /> },
|
||||
];
|
||||
|
|
@ -181,7 +184,7 @@ export default function App() {
|
|||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/domains" element={<Domains />} />
|
||||
<Route path="/test" element={<Test />} />
|
||||
<Route path="/discovery" element={<Discovery />} />
|
||||
<Route path="/logs" element={<Logs />} />
|
||||
<Route path="/settings/*" element={<Settings />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
|
|
|
|||
|
|
@ -1,512 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Stack,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Paper,
|
||||
Divider,
|
||||
Grid,
|
||||
Chip,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
PlayArrow as StartIcon,
|
||||
Stop as StopIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Add as AddIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { button_secondary, colors } from "@design";
|
||||
import { TestResultCard } from "@molecules/check/ResultCard";
|
||||
import { TestStatus } from "@atoms/check/Badge";
|
||||
import { useConfigLoad } from "@hooks/useConfig";
|
||||
import SettingTextField from "@atoms/common/B4TextField";
|
||||
import { useTestDomains } from "@hooks/useTestDomains";
|
||||
|
||||
interface TestResult {
|
||||
domain: string;
|
||||
status: TestStatus;
|
||||
duration: number;
|
||||
speed: number;
|
||||
bytes_read: number;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
is_baseline: boolean;
|
||||
improvement: number;
|
||||
status_code: number;
|
||||
}
|
||||
|
||||
interface TestSuite {
|
||||
id: string;
|
||||
status: TestStatus;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
total_checks: number;
|
||||
completed_checks: number;
|
||||
successful_checks: number;
|
||||
failed_checks: number;
|
||||
results: TestResult[];
|
||||
summary: {
|
||||
average_speed: number;
|
||||
average_improvement: number;
|
||||
fastest_domain: string;
|
||||
slowest_domain: string;
|
||||
success_rate: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface TestRunnerProps {
|
||||
onStart?: () => void;
|
||||
onComplete?: (suite: TestSuite) => void;
|
||||
}
|
||||
|
||||
export const TestRunner: React.FC<TestRunnerProps> = ({
|
||||
onStart,
|
||||
onComplete,
|
||||
}) => {
|
||||
const [running, setRunning] = useState(false);
|
||||
const [testId, setTestId] = useState<string | null>(null);
|
||||
const [suite, setSuite] = useState<TestSuite | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { config } = useConfigLoad();
|
||||
const { domains, addDomain, removeDomain, clearDomains, resetToDefaults } =
|
||||
useTestDomains();
|
||||
const [newDomain, setNewDomain] = useState("");
|
||||
|
||||
// Poll for test status
|
||||
useEffect(() => {
|
||||
if (!testId || !running) return;
|
||||
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/check/status?id=${testId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch test status");
|
||||
}
|
||||
|
||||
const data: TestSuite = (await response.json()) as TestSuite;
|
||||
setSuite(data);
|
||||
|
||||
if (
|
||||
data.status === "complete" ||
|
||||
data.status === "failed" ||
|
||||
data.status === "canceled"
|
||||
) {
|
||||
setRunning(false);
|
||||
if (onComplete) {
|
||||
onComplete(data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch test status:", err);
|
||||
setError(err instanceof Error ? err.message : "Unknown error");
|
||||
setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
void fetchStatus();
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [testId, running, onComplete]);
|
||||
|
||||
const startTest = async () => {
|
||||
if (domains.length === 0) {
|
||||
setError("Add at least one domain to test");
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setRunning(true);
|
||||
setSuite(null);
|
||||
|
||||
if (onStart) {
|
||||
onStart();
|
||||
}
|
||||
|
||||
try {
|
||||
const timeout = (config?.system.checker.timeout || 15) * 1e9;
|
||||
const maxConcurrent = config?.system.checker.max_concurrent || 5;
|
||||
|
||||
const response = await fetch("/api/check/start", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
timeout: timeout,
|
||||
max_concurrent: maxConcurrent,
|
||||
domains: domains,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || "Failed to start test");
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { id: string };
|
||||
setTestId(data.id);
|
||||
} catch (err) {
|
||||
console.error("Failed to start test:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to start test");
|
||||
setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelTest = async () => {
|
||||
if (!testId) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/check/cancel?id=${testId}`, { method: "DELETE" });
|
||||
setRunning(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to cancel test:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const resetTest = () => {
|
||||
setTestId(null);
|
||||
setSuite(null);
|
||||
setError(null);
|
||||
setRunning(false);
|
||||
};
|
||||
|
||||
const progress = suite
|
||||
? (suite.completed_checks / suite.total_checks) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
{/* Control Panel */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 3,
|
||||
bgcolor: colors.background.paper,
|
||||
border: `1px solid ${colors.border.default}`,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{/* Header with actions */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: colors.text.primary }}>
|
||||
DPI Bypass Test Suite
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{!running && !suite && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<StartIcon />}
|
||||
onClick={() => {
|
||||
void startTest();
|
||||
}}
|
||||
disabled={domains.length === 0}
|
||||
sx={{
|
||||
bgcolor: colors.secondary,
|
||||
"&:hover": { bgcolor: colors.primary },
|
||||
"&:disabled": {
|
||||
bgcolor: colors.accent.secondary,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Start Test
|
||||
</Button>
|
||||
)}
|
||||
{running && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<StopIcon />}
|
||||
onClick={() => {
|
||||
void cancelTest();
|
||||
}}
|
||||
sx={{
|
||||
borderColor: colors.quaternary,
|
||||
color: colors.quaternary,
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{suite && !running && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={resetTest}
|
||||
sx={{
|
||||
borderColor: colors.secondary,
|
||||
color: colors.secondary,
|
||||
}}
|
||||
>
|
||||
New Test
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
|
||||
{/* Domain Management Section */}
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ color: colors.text.primary }}
|
||||
>
|
||||
Domains to Test
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={resetToDefaults}
|
||||
disabled={running}
|
||||
sx={{ ...button_secondary, textTransform: "none" }}
|
||||
>
|
||||
Reset to Defaults
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={clearDomains}
|
||||
disabled={running || domains.length === 0}
|
||||
sx={{ ...button_secondary, textTransform: "none" }}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ sm: 12, md: 6 }}>
|
||||
{/* Domain Input */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: 1,
|
||||
pb: 2,
|
||||
width: "100%",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<SettingTextField
|
||||
fullWidth
|
||||
label="Add domain"
|
||||
value={newDomain}
|
||||
onChange={(e) => setNewDomain(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key === "Enter" ||
|
||||
e.key === "," ||
|
||||
e.key === "Tab"
|
||||
) {
|
||||
e.preventDefault();
|
||||
addDomain(newDomain);
|
||||
setNewDomain("");
|
||||
}
|
||||
}}
|
||||
placeholder="youtube.com"
|
||||
disabled={running}
|
||||
helperText="Press Enter or comma to add"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
addDomain(newDomain);
|
||||
setNewDomain("");
|
||||
}}
|
||||
disabled={running || !newDomain.trim()}
|
||||
sx={{
|
||||
bgcolor: colors.accent.secondary,
|
||||
color: colors.secondary,
|
||||
"&:hover": {
|
||||
bgcolor: colors.accent.secondaryHover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ sm: 12, md: 6 }}>
|
||||
{/* Domain Chips */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 1,
|
||||
p: 2,
|
||||
width: "100%",
|
||||
border: `1px solid ${colors.border.default}`,
|
||||
borderRadius: 1,
|
||||
bgcolor: colors.background.dark,
|
||||
}}
|
||||
>
|
||||
{domains.length === 0 ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
No domains added. Add domains above or click "Reset to
|
||||
Defaults"
|
||||
</Typography>
|
||||
) : (
|
||||
domains.map((domain) => (
|
||||
<Chip
|
||||
size="small"
|
||||
key={domain}
|
||||
label={domain}
|
||||
onDelete={() => removeDomain(domain)}
|
||||
disabled={running}
|
||||
sx={{
|
||||
bgcolor: colors.accent.primary,
|
||||
color: colors.secondary,
|
||||
"& .MuiChip-deleteIcon": {
|
||||
color: colors.secondary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Progress indicator */}
|
||||
{running && suite && (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Testing {suite.completed_checks} of {suite.total_checks}{" "}
|
||||
domains
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{progress.toFixed(0)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
bgcolor: colors.background.dark,
|
||||
"& .MuiLinearProgress-bar": {
|
||||
bgcolor: colors.secondary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Summary */}
|
||||
{suite && !running && suite.status === "complete" && (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 3,
|
||||
bgcolor: colors.background.paper,
|
||||
border: `1px solid ${colors.border.default}`,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ mb: 2, color: colors.text.primary }}>
|
||||
Test Summary
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2, borderColor: colors.border.default }} />
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="primary">
|
||||
{suite.successful_checks}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Successful
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="error">
|
||||
{suite.failed_checks}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Failed
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="secondary">
|
||||
{suite.summary.success_rate.toFixed(1)}%
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Success Rate
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Box sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" sx={{ color: colors.secondary }}>
|
||||
{(suite.summary.average_speed / 1024 / 1024).toFixed(2)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Avg Speed (MB/s)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Results Grid */}
|
||||
{suite?.results && suite.results.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ mb: 2, color: colors.text.primary }}>
|
||||
Test Results
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{suite.results.map((result) => (
|
||||
<Grid key={result.domain} size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<TestResultCard
|
||||
domain={result.domain}
|
||||
status={result.status}
|
||||
duration={result.duration / 1000000}
|
||||
speed={result.speed}
|
||||
improvement={result.improvement}
|
||||
error={result.error}
|
||||
status_code={result.status_code}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Tooltip,
|
||||
Snackbar,
|
||||
CircularProgress,
|
||||
Collapse,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
PlayArrow as StartIcon,
|
||||
|
|
@ -21,17 +22,38 @@ import {
|
|||
Refresh as RefreshIcon,
|
||||
Add as AddIcon,
|
||||
Speed as SpeedIcon,
|
||||
ExpandMore as ExpandIcon,
|
||||
ExpandLess as CollapseIcon,
|
||||
TrendingUp as ImprovementIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { button_secondary, colors } from "@design";
|
||||
import { useConfigLoad } from "@hooks/useConfig";
|
||||
import { useTestDomains } from "@hooks/useTestDomains";
|
||||
import { B4SetConfig } from "@/models/Config";
|
||||
import SettingTextField from "@atoms/common/B4TextField";
|
||||
import { AddSniModal } from "@organisms/domains/AddSniModal";
|
||||
import { generateDomainVariants } from "@utils";
|
||||
|
||||
// Strategy family types matching backend
|
||||
type StrategyFamily =
|
||||
| "none"
|
||||
| "tcp_frag"
|
||||
| "tls_record"
|
||||
| "oob"
|
||||
| "ip_frag"
|
||||
| "fake_sni"
|
||||
| "sack"
|
||||
| "syn_fake";
|
||||
|
||||
type DiscoveryPhase =
|
||||
| "baseline"
|
||||
| "strategy_detection"
|
||||
| "optimization"
|
||||
| "combination";
|
||||
|
||||
interface DomainPresetResult {
|
||||
preset_name: string;
|
||||
family?: StrategyFamily;
|
||||
phase?: DiscoveryPhase;
|
||||
status: "complete" | "failed";
|
||||
duration: number;
|
||||
speed: number;
|
||||
|
|
@ -47,6 +69,9 @@ interface DomainDiscoveryResult {
|
|||
best_speed: number;
|
||||
best_success: boolean;
|
||||
results: Record<string, DomainPresetResult>;
|
||||
working_families?: StrategyFamily[];
|
||||
baseline_speed?: number;
|
||||
improvement?: number;
|
||||
}
|
||||
|
||||
interface DiscoverySuite {
|
||||
|
|
@ -56,28 +81,57 @@ interface DiscoverySuite {
|
|||
end_time: string;
|
||||
total_checks: number;
|
||||
completed_checks: number;
|
||||
current_phase?: DiscoveryPhase;
|
||||
working_families?: string[];
|
||||
domain_discovery_results?: Record<string, DomainDiscoveryResult>;
|
||||
}
|
||||
|
||||
interface ConfigDetail {
|
||||
category: string;
|
||||
settings: Array<{ label: string; value: string }>;
|
||||
interface DiscoveryStartResponse {
|
||||
id: string;
|
||||
total_domains: number;
|
||||
total_clusters: number;
|
||||
estimated_tests: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Friendly names for strategy families
|
||||
const familyNames: Record<StrategyFamily, string> = {
|
||||
none: "Baseline",
|
||||
tcp_frag: "TCP Fragmentation",
|
||||
tls_record: "TLS Record Split",
|
||||
oob: "Out-of-Band",
|
||||
ip_frag: "IP Fragmentation",
|
||||
fake_sni: "Fake SNI",
|
||||
sack: "SACK Drop",
|
||||
syn_fake: "SYN Fake",
|
||||
};
|
||||
|
||||
// Friendly names for phases
|
||||
const phaseNames: Record<DiscoveryPhase, string> = {
|
||||
baseline: "Baseline Test",
|
||||
strategy_detection: "Strategy Detection",
|
||||
optimization: "Optimization",
|
||||
combination: "Combination Test",
|
||||
};
|
||||
|
||||
export const DiscoveryRunner: React.FC = () => {
|
||||
const [running, setRunning] = useState(false);
|
||||
const [suiteId, setSuiteId] = useState<string | null>(null);
|
||||
const [suite, setSuite] = useState<DiscoverySuite | null>(null);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [addingPreset, setAddingPreset] = useState<string | null>(null);
|
||||
const [variants, setVariants] = useState<string[]>([]);
|
||||
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
|
||||
const [expandedDomains, setExpandedDomains] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [snackbar, setSnackbar] = useState<{
|
||||
open: boolean;
|
||||
message: string;
|
||||
severity: "success" | "error";
|
||||
}>({ open: false, message: "", severity: "success" });
|
||||
const { config } = useConfigLoad();
|
||||
|
||||
const { domains, addDomain, removeDomain, clearDomains, resetToDefaults } =
|
||||
useTestDomains();
|
||||
const [newDomain, setNewDomain] = useState("");
|
||||
|
|
@ -95,13 +149,25 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
setVariantModal({ open: true, domain, result });
|
||||
};
|
||||
|
||||
const toggleDomainExpand = (domain: string) => {
|
||||
setExpandedDomains((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(domain)) {
|
||||
next.delete(domain);
|
||||
} else {
|
||||
next.add(domain);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Poll for discovery status
|
||||
useEffect(() => {
|
||||
if (!suiteId || !running) return;
|
||||
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/check/status?id=${suiteId}`);
|
||||
const response = await fetch(`/api/discovery/status?id=${suiteId}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch discovery status");
|
||||
|
||||
const data = (await response.json()) as DiscoverySuite;
|
||||
|
|
@ -119,7 +185,7 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
|
||||
const interval = setInterval(() => {
|
||||
void fetchStatus();
|
||||
}, 2000);
|
||||
}, 1500); // Faster polling for better UX
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [suiteId, running]);
|
||||
|
|
@ -135,15 +201,10 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
setSuite(null);
|
||||
|
||||
try {
|
||||
const timeout = (config?.system.checker.timeout || 15) * 1e9;
|
||||
const maxConcurrent = config?.system.checker.max_concurrent || 3;
|
||||
|
||||
const response = await fetch("/api/check/discovery", {
|
||||
const response = await fetch("/api/discovery", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
timeout: timeout,
|
||||
max_concurrent: maxConcurrent,
|
||||
domains: domains,
|
||||
}),
|
||||
});
|
||||
|
|
@ -153,7 +214,7 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
throw new Error(text || "Failed to start discovery");
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { id: string; message: string };
|
||||
const data = (await response.json()) as DiscoveryStartResponse;
|
||||
setSuiteId(data.id);
|
||||
} catch (err) {
|
||||
console.error("Failed to start discovery:", err);
|
||||
|
|
@ -168,7 +229,7 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
if (!suiteId) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/check/cancel?id=${suiteId}`, { method: "DELETE" });
|
||||
await fetch(`/api/discovery/cancel?id=${suiteId}`, { method: "DELETE" });
|
||||
setRunning(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to cancel discovery:", err);
|
||||
|
|
@ -180,6 +241,7 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
setSuite(null);
|
||||
setError(null);
|
||||
setRunning(false);
|
||||
setExpandedDomains(new Set());
|
||||
};
|
||||
|
||||
const confirmAddStrategy = async () => {
|
||||
|
|
@ -197,7 +259,7 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
`${variantModal.domain}-${variantModal.result.preset_name}`
|
||||
);
|
||||
|
||||
const response = await fetch("/api/check/add", {
|
||||
const response = await fetch("/api/discovery/add", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(configToAdd),
|
||||
|
|
@ -231,131 +293,22 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
? (suite.completed_checks / suite.total_checks) * 100
|
||||
: 0;
|
||||
|
||||
function formatConfigDetails(presetName: string): ConfigDetail[] {
|
||||
const details: ConfigDetail[] = [];
|
||||
// Group results by phase for display
|
||||
const groupResultsByPhase = (results: Record<string, DomainPresetResult>) => {
|
||||
const grouped: Record<DiscoveryPhase, DomainPresetResult[]> = {
|
||||
baseline: [],
|
||||
strategy_detection: [],
|
||||
optimization: [],
|
||||
combination: [],
|
||||
};
|
||||
|
||||
// TCP
|
||||
details.push({
|
||||
category: "TCP Configuration",
|
||||
settings: [
|
||||
{ label: "Connection Bytes", value: "19 bytes" },
|
||||
{
|
||||
label: "Segment Delay",
|
||||
value: presetName.includes("delay") ? "5-10ms" : "0ms",
|
||||
},
|
||||
],
|
||||
Object.values(results).forEach((result) => {
|
||||
const phase = result.phase || "strategy_detection";
|
||||
grouped[phase].push(result);
|
||||
});
|
||||
|
||||
// Fragmentation
|
||||
if (presetName.includes("tcp-frag")) {
|
||||
const position = presetName.includes("pos1")
|
||||
? "1"
|
||||
: presetName.includes("pos2")
|
||||
? "2"
|
||||
: "Variable";
|
||||
details.push({
|
||||
category: "Fragmentation",
|
||||
settings: [
|
||||
{ label: "Strategy", value: "TCP" },
|
||||
{ label: "SNI Position", value: position },
|
||||
{
|
||||
label: "Reverse Order",
|
||||
value: presetName.includes("reverse") ? "Yes" : "No",
|
||||
},
|
||||
{
|
||||
label: "Middle SNI",
|
||||
value: presetName.includes("middle") ? "Yes" : "No",
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (presetName.includes("ip-frag")) {
|
||||
details.push({
|
||||
category: "Fragmentation",
|
||||
settings: [
|
||||
{ label: "Strategy", value: "IP-level" },
|
||||
{ label: "SNI Position", value: "1" },
|
||||
{
|
||||
label: "Reverse Order",
|
||||
value: presetName.includes("reverse") ? "Yes" : "No",
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (presetName.includes("no-frag")) {
|
||||
details.push({
|
||||
category: "Fragmentation",
|
||||
settings: [{ label: "Strategy", value: "None" }],
|
||||
});
|
||||
}
|
||||
|
||||
// Faking
|
||||
if (presetName.includes("no-fake")) {
|
||||
details.push({
|
||||
category: "Fake Packets",
|
||||
settings: [{ label: "Status", value: "Disabled" }],
|
||||
});
|
||||
} else if (presetName.includes("fake")) {
|
||||
const ttl = presetName.includes("ttl-low") ? "3" : "5-8";
|
||||
const strategy = presetName.includes("randseq")
|
||||
? "Random Seq"
|
||||
: presetName.includes("md5sum")
|
||||
? "MD5"
|
||||
: "Past Seq";
|
||||
const count = presetName.includes("multi")
|
||||
? "5"
|
||||
: presetName.includes("aggressive")
|
||||
? "3-5"
|
||||
: "1-2";
|
||||
|
||||
details.push({
|
||||
category: "Fake Packets",
|
||||
settings: [
|
||||
{ label: "TTL", value: ttl },
|
||||
{ label: "Strategy", value: strategy },
|
||||
{ label: "Count", value: count },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
details.push({
|
||||
category: "Fake Packets",
|
||||
settings: [
|
||||
{ label: "TTL", value: "8" },
|
||||
{ label: "Strategy", value: "Past Seq" },
|
||||
{ label: "Count", value: "1" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// UDP/QUIC
|
||||
if (presetName.includes("quic-drop")) {
|
||||
details.push({
|
||||
category: "UDP/QUIC",
|
||||
settings: [
|
||||
{ label: "Mode", value: "Drop" },
|
||||
{ label: "QUIC Filter", value: "All" },
|
||||
],
|
||||
});
|
||||
} else if (presetName.includes("quic-fake")) {
|
||||
details.push({
|
||||
category: "UDP/QUIC",
|
||||
settings: [
|
||||
{ label: "Mode", value: "Fake & Frag" },
|
||||
{ label: "Fake Count", value: "10" },
|
||||
{ label: "Fake Size", value: "128 bytes" },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
details.push({
|
||||
category: "UDP/QUIC",
|
||||
settings: [
|
||||
{ label: "Mode", value: "Fake & Frag" },
|
||||
{ label: "Fake Count", value: "6" },
|
||||
{ label: "QUIC Filter", value: "Disabled" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
|
|
@ -378,9 +331,18 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: colors.text.primary }}>
|
||||
Configuration Discovery
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ color: colors.text.primary }}>
|
||||
Configuration Discovery
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: colors.text.secondary }}
|
||||
>
|
||||
Hierarchical testing: Strategy Detection → Optimization →
|
||||
Combination
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{!running && !suite && (
|
||||
<Button
|
||||
|
|
@ -433,12 +395,6 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Alert severity="warning" sx={{ bgcolor: colors.accent.tertiary }}>
|
||||
<strong>Warning:</strong> Discovery mode will temporarily apply
|
||||
different configurations to test effectiveness. This may briefly
|
||||
affect your service traffic during testing.
|
||||
</Alert>
|
||||
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
|
||||
{/* Domain Management Section */}
|
||||
|
|
@ -477,7 +433,6 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ sm: 12, md: 6 }}>
|
||||
{/* Domain Input */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
|
@ -526,7 +481,6 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ sm: 12, md: 6 }}>
|
||||
{/* Domain Chips */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
|
@ -537,6 +491,8 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
border: `1px solid ${colors.border.default}`,
|
||||
borderRadius: 1,
|
||||
bgcolor: colors.background.dark,
|
||||
maxHeight: 120,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{domains.length === 0 ? (
|
||||
|
|
@ -584,10 +540,23 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Testing configurations: {suite.completed_checks} of{" "}
|
||||
{suite.total_checks} checks completed
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{suite.current_phase && (
|
||||
<Chip
|
||||
label={phaseNames[suite.current_phase]}
|
||||
size="small"
|
||||
sx={{
|
||||
mr: 1,
|
||||
bgcolor: colors.accent.secondary,
|
||||
color: colors.secondary,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{suite.completed_checks} of {suite.total_checks} checks
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{progress.toFixed(0)}%
|
||||
</Typography>
|
||||
|
|
@ -610,16 +579,21 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Results Table */}
|
||||
{/* Results */}
|
||||
{suite?.domain_discovery_results &&
|
||||
Object.keys(suite.domain_discovery_results).length > 0 && (
|
||||
<Stack spacing={2}>
|
||||
{Object.values(suite.domain_discovery_results)
|
||||
.sort((a, b) => b.best_speed - a.best_speed)
|
||||
.map((domainResult) => {
|
||||
const configDetails = domainResult.best_success
|
||||
? formatConfigDetails(domainResult.best_preset)
|
||||
: [];
|
||||
const isExpanded = expandedDomains.has(domainResult.domain);
|
||||
const groupedResults = groupResultsByPhase(
|
||||
domainResult.results
|
||||
);
|
||||
const successCount = Object.values(domainResult.results).filter(
|
||||
(r) => r.status === "complete"
|
||||
).length;
|
||||
const totalCount = Object.keys(domainResult.results).length;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
|
|
@ -640,11 +614,16 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => toggleDomainExpand(domainResult.domain)}
|
||||
>
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", gap: 2 }}
|
||||
>
|
||||
<IconButton size="small">
|
||||
{isExpanded ? <CollapseIcon /> : <ExpandIcon />}
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: colors.text.primary }}
|
||||
|
|
@ -670,6 +649,25 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<Chip
|
||||
label={`${successCount}/${totalCount} configs`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ borderColor: colors.border.light }}
|
||||
/>
|
||||
{domainResult.improvement &&
|
||||
domainResult.improvement > 0 && (
|
||||
<Chip
|
||||
icon={<ImprovementIcon />}
|
||||
label={`+${domainResult.improvement.toFixed(0)}%`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: colors.accent.secondary,
|
||||
color: colors.secondary,
|
||||
"& .MuiChip-icon": { color: colors.secondary },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
|
|
@ -679,251 +677,262 @@ export const DiscoveryRunner: React.FC = () => {
|
|||
? `${(domainResult.best_speed / 1024 / 1024).toFixed(
|
||||
2
|
||||
)} MB/s`
|
||||
: "No successful config"}
|
||||
: "No working config"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Configuration Details */}
|
||||
{/* Best Configuration Quick View (always visible) */}
|
||||
{domainResult.best_success && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: colors.background.default,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: `1px solid ${colors.border.default}`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
sx={{ display: "flex", alignItems: "center", gap: 2 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<SpeedIcon sx={{ color: colors.secondary }} />
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
textTransform: "uppercase",
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
variant="caption"
|
||||
sx={{ color: colors.text.secondary }}
|
||||
>
|
||||
Best Configuration
|
||||
</Typography>
|
||||
<Chip
|
||||
icon={<SpeedIcon />}
|
||||
label={`${domainResult.best_preset} • ${(
|
||||
domainResult.best_speed /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)} MB/s`}
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
bgcolor: colors.accent.secondary,
|
||||
color: colors.secondary,
|
||||
color: colors.text.primary,
|
||||
fontWeight: 600,
|
||||
"& .MuiChip-icon": {
|
||||
color: colors.secondary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{domainResult.best_preset}
|
||||
{domainResult.results[domainResult.best_preset]
|
||||
?.family && (
|
||||
<Chip
|
||||
label={
|
||||
familyNames[
|
||||
domainResult.results[
|
||||
domainResult.best_preset
|
||||
].family!
|
||||
]
|
||||
}
|
||||
size="small"
|
||||
sx={{ ml: 1, bgcolor: colors.accent.primary }}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={
|
||||
addingPreset ===
|
||||
`${domainResult.domain}-${domainResult.best_preset}` ? (
|
||||
<CircularProgress size={18} color="inherit" />
|
||||
) : (
|
||||
<AddIcon />
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const bestResult =
|
||||
domainResult.results[domainResult.best_preset];
|
||||
void handleAddStrategy(
|
||||
domainResult.domain,
|
||||
bestResult
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
addingPreset ===
|
||||
`${domainResult.domain}-${domainResult.best_preset}`
|
||||
}
|
||||
sx={{
|
||||
bgcolor: colors.secondary,
|
||||
color: colors.background.default,
|
||||
fontWeight: 600,
|
||||
"&:hover": {
|
||||
bgcolor: colors.primary,
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: `0 4px 8px ${colors.secondary}44`,
|
||||
},
|
||||
transition: "all 0.2s",
|
||||
"&:disabled": {
|
||||
bgcolor: colors.accent.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{addingPreset ===
|
||||
`${domainResult.domain}-${domainResult.best_preset}`
|
||||
? "Adding..."
|
||||
: "Use This Strategy"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={
|
||||
addingPreset ===
|
||||
`${domainResult.domain}-${domainResult.best_preset}` ? (
|
||||
<CircularProgress size={18} color="inherit" />
|
||||
) : (
|
||||
<AddIcon />
|
||||
)
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const bestResult =
|
||||
domainResult.results[domainResult.best_preset];
|
||||
void handleAddStrategy(
|
||||
domainResult.domain,
|
||||
bestResult
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
addingPreset ===
|
||||
`${domainResult.domain}-${domainResult.best_preset}`
|
||||
}
|
||||
sx={{
|
||||
bgcolor: colors.secondary,
|
||||
color: colors.background.default,
|
||||
"&:hover": { bgcolor: colors.primary },
|
||||
}}
|
||||
>
|
||||
Use This Strategy
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Expanded Details */}
|
||||
<Collapse in={isExpanded}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* Working Families */}
|
||||
{domainResult.working_families &&
|
||||
domainResult.working_families.length > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
mb: 1,
|
||||
textTransform: "uppercase",
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
>
|
||||
Working Strategy Families
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
flexWrap="wrap"
|
||||
gap={1}
|
||||
>
|
||||
{domainResult.working_families.map((family) => (
|
||||
<Chip
|
||||
key={family}
|
||||
label={familyNames[family]}
|
||||
sx={{
|
||||
bgcolor: colors.accent.secondary,
|
||||
color: colors.secondary,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider
|
||||
sx={{ my: 2, borderColor: colors.border.default }}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{configDetails.map((detail, idx) => (
|
||||
<Grid key={idx} size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<Box
|
||||
{/* Results by Phase */}
|
||||
{(
|
||||
[
|
||||
"baseline",
|
||||
"strategy_detection",
|
||||
"optimization",
|
||||
"combination",
|
||||
] as DiscoveryPhase[]
|
||||
)
|
||||
.filter((phase) => groupedResults[phase].length > 0)
|
||||
.map((phase) => (
|
||||
<Box key={phase} sx={{ mb: 3 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: colors.background.dark,
|
||||
borderRadius: 1,
|
||||
border: `1px solid ${colors.border.light}`,
|
||||
color: colors.text.secondary,
|
||||
mb: 1.5,
|
||||
textTransform: "uppercase",
|
||||
fontSize: "0.7rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color: colors.secondary,
|
||||
fontWeight: 600,
|
||||
mb: 1.5,
|
||||
textTransform: "uppercase",
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
>
|
||||
{detail.category}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{detail.settings.map((setting, i) => (
|
||||
<Box key={i}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
display: "block",
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
{phaseNames[phase]}
|
||||
<Chip
|
||||
label={groupedResults[phase].length}
|
||||
size="small"
|
||||
sx={{ height: 18, fontSize: "0.65rem" }}
|
||||
/>
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
flexWrap="wrap"
|
||||
gap={1}
|
||||
>
|
||||
{groupedResults[phase]
|
||||
.sort((a, b) => b.speed - a.speed)
|
||||
.map((result) => (
|
||||
<Box
|
||||
key={result.preset_name}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
result.status === "complete"
|
||||
? `${result.preset_name}: ${(
|
||||
result.speed /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)} MB/s`
|
||||
: `${result.preset_name}: ${
|
||||
result.error || "Failed"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{setting.label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: colors.text.primary,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{setting.value}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${result.preset_name}: ${
|
||||
result.status === "complete"
|
||||
? `${(
|
||||
result.speed /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)} MB/s`
|
||||
: "Failed"
|
||||
}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor:
|
||||
result.preset_name ===
|
||||
domainResult.best_preset
|
||||
? colors.accent.secondary
|
||||
: colors.background.dark,
|
||||
color:
|
||||
result.status === "complete"
|
||||
? colors.text.primary
|
||||
: colors.quaternary,
|
||||
border:
|
||||
result.preset_name ===
|
||||
domainResult.best_preset
|
||||
? `2px solid ${colors.secondary}`
|
||||
: `1px solid ${colors.border.light}`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
{result.status === "complete" &&
|
||||
result.preset_name !==
|
||||
domainResult.best_preset && (
|
||||
<Tooltip title="Use this configuration">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
void handleAddStrategy(
|
||||
domainResult.domain,
|
||||
result
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
addingPreset ===
|
||||
`${domainResult.domain}-${result.preset_name}`
|
||||
}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
bgcolor: colors.background.dark,
|
||||
border: `1px solid ${colors.border.light}`,
|
||||
"&:hover": {
|
||||
bgcolor:
|
||||
colors.accent.secondary,
|
||||
borderColor: colors.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* All Tested Configs */}
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
mb: 1,
|
||||
textTransform: "uppercase",
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
>
|
||||
All Tested Configurations
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
flexWrap="wrap"
|
||||
gap={1}
|
||||
>
|
||||
{Object.values(domainResult.results)
|
||||
.sort((a, b) => b.speed - a.speed)
|
||||
.map((result) => (
|
||||
<Box
|
||||
key={result.preset_name}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={`${result.preset_name}: ${
|
||||
result.status === "complete"
|
||||
? `${(
|
||||
result.speed /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)} MB/s`
|
||||
: "Failed"
|
||||
}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor:
|
||||
result.preset_name ===
|
||||
domainResult.best_preset
|
||||
? colors.accent.secondary
|
||||
: colors.background.dark,
|
||||
color:
|
||||
result.status === "complete"
|
||||
? colors.text.primary
|
||||
: colors.quaternary,
|
||||
border:
|
||||
result.preset_name ===
|
||||
domainResult.best_preset
|
||||
? `2px solid ${colors.secondary}`
|
||||
: `1px solid ${colors.border.light}`,
|
||||
}}
|
||||
/>
|
||||
{result.status === "complete" &&
|
||||
result.preset_name !==
|
||||
domainResult.best_preset && (
|
||||
<Tooltip title="Use this configuration">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
void handleAddStrategy(
|
||||
domainResult.domain,
|
||||
result
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
addingPreset ===
|
||||
`${domainResult.domain}-${result.preset_name}`
|
||||
}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
bgcolor: colors.background.dark,
|
||||
border: `1px solid ${colors.border.light}`,
|
||||
"&:hover": {
|
||||
bgcolor: colors.accent.secondary,
|
||||
borderColor: colors.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Collapse>
|
||||
|
||||
{/* Failed state */}
|
||||
{!domainResult.best_success && (
|
||||
|
|
@ -25,27 +25,30 @@ export const CheckerSettings: React.FC<CheckerSettingsProps> = ({
|
|||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, lg: 6 }}>
|
||||
<B4Slider
|
||||
label="Max Concurrent Tests"
|
||||
value={config.system.checker.max_concurrent}
|
||||
label="Discovery Timeout"
|
||||
value={config.system.checker.discovery_timeout || 5}
|
||||
onChange={(value) =>
|
||||
onChange("system.checker.max_concurrent", value)
|
||||
onChange("system.checker.discovery_timeout", value)
|
||||
}
|
||||
min={1}
|
||||
max={20}
|
||||
min={3}
|
||||
max={30}
|
||||
step={1}
|
||||
helperText="Maximum number of concurrent tests"
|
||||
valueSuffix=" sec"
|
||||
helperText="Timeout per preset during discovery"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 6 }}>
|
||||
<B4Slider
|
||||
label="Test Timeout"
|
||||
value={config.system.checker.timeout}
|
||||
onChange={(value) => onChange("system.checker.timeout", value)}
|
||||
min={1}
|
||||
max={120}
|
||||
step={1}
|
||||
valueSuffix=" sec"
|
||||
helperText="Domain request timeout"
|
||||
label="Config Propagation Delay"
|
||||
value={config.system.checker.config_propagate_ms || 1500}
|
||||
onChange={(value) =>
|
||||
onChange("system.checker.config_propagate_ms", value)
|
||||
}
|
||||
min={500}
|
||||
max={5000}
|
||||
step={100}
|
||||
valueSuffix=" ms"
|
||||
helperText="Delay for config to propagate to workers (increase on slow devices)"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
import { Container, Alert, Stack, Tabs, Tab } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { TestRunner } from "@organisms/check/Runner";
|
||||
import { DiscoveryRunner } from "@organisms/check/Discovery";
|
||||
import { colors } from "@design";
|
||||
|
||||
export default function Test() {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
<Container
|
||||
maxWidth={false}
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
py: 3,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, newValue: number) => setActiveTab(newValue)}
|
||||
sx={{
|
||||
borderBottom: `1px solid ${colors.border.default}`,
|
||||
"& .MuiTab-root": {
|
||||
color: colors.text.secondary,
|
||||
"&.Mui-selected": {
|
||||
color: colors.secondary,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab label="Quick Test" />
|
||||
<Tab label="Discovery" />
|
||||
</Tabs>
|
||||
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
bgcolor: colors.accent.primary,
|
||||
border: `1px solid ${colors.secondary}44`,
|
||||
}}
|
||||
>
|
||||
<strong>Quick Test:</strong> Test your current configuration
|
||||
against the domains you specify below. This validates your
|
||||
existing DPI bypass settings without making any changes.
|
||||
</Alert>
|
||||
<TestRunner />
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Alert severity="warning">
|
||||
This feature is EXPERIMENTAL and may affect your current
|
||||
configuration.
|
||||
</Alert>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
bgcolor: colors.accent.primary,
|
||||
border: `1px solid ${colors.secondary}44`,
|
||||
}}
|
||||
>
|
||||
<strong>Configuration Discovery:</strong> Automatically test
|
||||
multiple configuration presets to find the most effective DPI
|
||||
bypass settings for the domains you specify below. B4 will
|
||||
temporarily apply different configurations and measure their
|
||||
performance.
|
||||
</Alert>
|
||||
<DiscoveryRunner />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
38
src/http/ui/src/components/pages/Discovery.tsx
Normal file
38
src/http/ui/src/components/pages/Discovery.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { Container, Alert, Stack } from "@mui/material";
|
||||
import { DiscoveryRunner } from "@/components/organisms/discovery/Discovery";
|
||||
import { colors } from "@design";
|
||||
|
||||
export default function Test() {
|
||||
return (
|
||||
<Container
|
||||
maxWidth={false}
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
py: 3,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
<Alert severity="warning">
|
||||
This feature is EXPERIMENTAL and may affect your current
|
||||
configuration.
|
||||
</Alert>
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{
|
||||
bgcolor: colors.accent.primary,
|
||||
border: `1px solid ${colors.secondary}44`,
|
||||
}}
|
||||
>
|
||||
<strong>Configuration Discovery:</strong> Automatically test multiple
|
||||
configuration presets to find the most effective DPI bypass settings
|
||||
for the domains you specify below. B4 will temporarily apply different
|
||||
configurations and measure their performance.
|
||||
</Alert>
|
||||
<DiscoveryRunner />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ import {
|
|||
Settings as SettingsIcon,
|
||||
Warning as WarningIcon,
|
||||
Layers as LayersIcon,
|
||||
Monitor as MonitorIcon,
|
||||
Science as DiscoveryIcon,
|
||||
Language as LanguageIcon,
|
||||
Cloud as ApiIcon,
|
||||
CameraAlt as CaptureIcon,
|
||||
|
|
@ -34,7 +34,7 @@ import { CaptureSettings } from "@organisms/settings/Capture";
|
|||
import { NetworkSettings } from "@organisms/settings/Network";
|
||||
import { LoggingSettings } from "@organisms/settings/Logging";
|
||||
import { FeatureSettings } from "@organisms/settings/Feature";
|
||||
import { CheckerSettings } from "@organisms/settings/Checker";
|
||||
import { CheckerSettings } from "@/components/organisms/settings/Checker";
|
||||
import { ControlSettings } from "@organisms/settings/Control";
|
||||
import {
|
||||
SetsManager,
|
||||
|
|
@ -78,7 +78,7 @@ enum TABS {
|
|||
SETS = 0,
|
||||
GENERAL,
|
||||
DOMAINS,
|
||||
TESTING,
|
||||
DISCOVERY,
|
||||
API,
|
||||
CAPTURE,
|
||||
}
|
||||
|
|
@ -110,10 +110,10 @@ const SETTING_CATEGORIES = [
|
|||
requiresRestart: false,
|
||||
},
|
||||
{
|
||||
id: TABS.TESTING,
|
||||
path: "testing",
|
||||
label: "Testing",
|
||||
icon: <MonitorIcon />,
|
||||
id: TABS.DISCOVERY,
|
||||
path: "discovery",
|
||||
label: "Discovery",
|
||||
icon: <DiscoveryIcon />,
|
||||
description: "DPI bypass domains testing",
|
||||
requiresRestart: false,
|
||||
},
|
||||
|
|
@ -212,8 +212,8 @@ export default function Settings() {
|
|||
JSON.stringify(config.system.geo) !==
|
||||
JSON.stringify(originalConfig.system.geo),
|
||||
|
||||
// Testing
|
||||
[TABS.TESTING]:
|
||||
// Discovery
|
||||
[TABS.DISCOVERY]:
|
||||
JSON.stringify(config.system.checker) !==
|
||||
JSON.stringify(originalConfig.system.checker),
|
||||
|
||||
|
|
@ -549,7 +549,7 @@ export default function Settings() {
|
|||
<ApiSettings config={config} onChange={handleChange} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={validTab} index={TABS.TESTING}>
|
||||
<TabPanel value={validTab} index={TABS.DISCOVERY}>
|
||||
<CheckerSettings config={config} onChange={handleChange} />
|
||||
</TabPanel>
|
||||
|
||||
|
|
|
|||
|
|
@ -108,9 +108,9 @@ export interface QueueConfig {
|
|||
}
|
||||
|
||||
export interface CheckerConfig {
|
||||
timeout: number;
|
||||
max_concurrent: number;
|
||||
domains: string[];
|
||||
discovery_timeout: number;
|
||||
config_propagate_ms: number;
|
||||
}
|
||||
|
||||
export type WindowMode = "off" | "oscillate" | "zero" | "random" | "escalate";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue