diff --git a/src/convert/convert.go b/src/convert/convert.go index 7886557e..626aa6ee 100644 --- a/src/convert/convert.go +++ b/src/convert/convert.go @@ -118,6 +118,7 @@ func Analyze(input string, opts Options) (*Result, error) { ProfileDomains: opts.ProfileDomains, ProfileModel: spec.ProfileModel, BreakKeys: spec.ProfileBreak, + Defaults: spec.Defaults, }) noteUnaccounted(resolved, notes) diff --git a/src/convert/convert_test.go b/src/convert/convert_test.go index b74d31e6..e49ed6a7 100644 --- a/src/convert/convert_test.go +++ b/src/convert/convert_test.go @@ -4,8 +4,6 @@ import ( "errors" "strings" "testing" - - "github.com/daniellavrushin/b4/config" ) func analyze(t *testing.T, line string) *Result { @@ -37,303 +35,6 @@ func hasField(n Note, field string) bool { return false } -func TestAnalyze_Splitting(t *testing.T) { - tests := []struct { - name string - line string - strategy string - middleSNI bool - sniPosition int - }{ - {"sniStart", "-s1+s", "tcp", true, 0}, - {"sniMiddle", "-s0+sm", "tcp", true, 0}, - {"fixedPosition", "-s5", "tcp", false, 5}, - {"firstByte", "-s1", "tcp", false, 1}, - {"disorder", "-d0+sm", "disorder", true, 0}, - {"splitAndDisorder", "-s1 -d0+sm", "combo", true, 0}, - {"noSplit", "-t8", "none", true, 1}, - {"negativeOffset", "-s-1", "tcp", true, 0}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - res := analyze(t, tt.line) - set := res.Sets[0] - if set.Fragmentation.Strategy != tt.strategy { - t.Fatalf("strategy: got %q, want %q", set.Fragmentation.Strategy, tt.strategy) - } - if tt.strategy == config.ConfigNone { - return - } - if set.Fragmentation.MiddleSNI != tt.middleSNI { - t.Fatalf("middle_sni: got %v, want %v", set.Fragmentation.MiddleSNI, tt.middleSNI) - } - if set.Fragmentation.SNIPosition != tt.sniPosition { - t.Fatalf("sni_position: got %d, want %d", set.Fragmentation.SNIPosition, tt.sniPosition) - } - }) - } -} - -func TestAnalyze_OOBUsesByedpiDefaultByte(t *testing.T) { - res := analyze(t, "-o1") - set := res.Sets[0] - if set.Fragmentation.Strategy != "oob" { - t.Fatalf("strategy: got %q", set.Fragmentation.Strategy) - } - if set.Fragmentation.OOBPosition != 1 { - t.Fatalf("oob_position: got %d", set.Fragmentation.OOBPosition) - } - if set.Fragmentation.OOBChar != 'a' { - t.Fatalf("oob_char: got %d, want %d (byedpi default), b4 default is %d", - set.Fragmentation.OOBChar, 'a', config.DefaultSetConfig.Fragmentation.OOBChar) - } -} - -func TestAnalyze_OOBByteOverride(t *testing.T) { - res := analyze(t, "-o1 -eb") - if got := res.Sets[0].Fragmentation.OOBChar; got != 'b' { - t.Fatalf("oob_char: got %d, want %d", got, 'b') - } -} - -func TestAnalyze_TLSRecord(t *testing.T) { - res := analyze(t, "-r2") - set := res.Sets[0] - if set.Fragmentation.Strategy != "tls" || set.Fragmentation.TLSRecordPosition != 2 { - t.Fatalf("got strategy=%q pos=%d", set.Fragmentation.Strategy, set.Fragmentation.TLSRecordPosition) - } -} - -func TestAnalyze_FakeDefaults(t *testing.T) { - res := analyze(t, "-f-1") - set := res.Sets[0] - if !set.Faking.SNI { - t.Fatal("expected faking.sni to be enabled") - } - if set.Faking.Strategy != "ttl" || !set.Faking.ApplyTTL { - t.Fatalf("got strategy=%q apply_ttl=%v", set.Faking.Strategy, set.Faking.ApplyTTL) - } - if set.Faking.TTL != byedpiDefaultFakeTTL { - t.Fatalf("ttl: got %d, want %d", set.Faking.TTL, byedpiDefaultFakeTTL) - } -} - -func TestAnalyze_FakeSNIBecomesGeneratedPayload(t *testing.T) { - res := analyze(t, "-f-1 -Qr -n https://www.gosuslugi.ru/") - set := res.Sets[0] - if set.Faking.SNIType != config.FakePayloadDomain { - t.Fatalf("sni_type: got %d, want %d", set.Faking.SNIType, config.FakePayloadDomain) - } - if set.Faking.PayloadDomain != "www.gosuslugi.ru" { - t.Fatalf("payload_domain: got %q", set.Faking.PayloadDomain) - } - if len(set.Faking.TLSMod) != 1 || set.Faking.TLSMod[0] != "rnd" { - t.Fatalf("tls_mod: got %v", set.Faking.TLSMod) - } -} - -func TestAnalyze_MD5SigWithoutFakeIsDegenerate(t *testing.T) { - res := analyze(t, "-d0+sm -S") - n := noteFor(t, res, "-S") - if n.Status != StatusDegenerate || n.Reason != "requiresFake" { - t.Fatalf("got %+v", n) - } -} - -func TestAnalyze_RepeatsWithoutSkipIsDegenerate(t *testing.T) { - res := analyze(t, "-d1:11+sm") - n := noteFor(t, res, "-d1:11+sm") - if n.Status != StatusDegenerate || n.Reason != "repeatsWithoutSkip" { - t.Fatalf("got %+v", n) - } -} - -func TestAnalyze_RepeatsWithSkipIsApproximated(t *testing.T) { - res := analyze(t, "-s1:3:5") - n := noteFor(t, res, "-s1:3:5") - if n.Status != StatusApproximated || n.Reason != "repeatsUnsupported" { - t.Fatalf("got %+v", n) - } -} - -func TestAnalyze_HostsInlineBecomeTargets(t *testing.T) { - res := analyze(t, "-H:youtube.com,googlevideo.com -s1+s") - set := res.Sets[0] - if len(set.Targets.SNIDomains) != 2 { - t.Fatalf("sni_domains: got %v", set.Targets.SNIDomains) - } - if !set.Enabled { - t.Fatal("a set with targets should be enabled") - } -} - -func TestAnalyze_HostsFileIsUnresolved(t *testing.T) { - res := analyze(t, "-H /etc/byedpi/hosts.txt -s1+s") - if len(res.Unresolved) != 1 || res.Unresolved[0].Kind != "hostlist" { - t.Fatalf("unresolved: got %+v", res.Unresolved) - } - if res.Sets[0].Enabled { - t.Fatal("a set with no resolved targets must stay disabled") - } -} - -func TestAnalyze_ProxyRuntimeIsNotApplicable(t *testing.T) { - res := analyze(t, "-i 0.0.0.0 -p 1080 -c 512 -s1+s") - for _, tok := range []string{"-i 0.0.0.0", "-p 1080", "-c 512"} { - n := noteFor(t, res, tok) - if n.Status != StatusNotApplicable { - t.Fatalf("%s: got %+v", tok, n) - } - } - if res.Fidelity.NotApplicable != 3 { - t.Fatalf("not_applicable: got %d", res.Fidelity.NotApplicable) - } - bare := analyze(t, "-s1+s") - if res.Fidelity.Score != bare.Fidelity.Score { - t.Fatalf("proxy plumbing must not change the score: got %d with, %d without", - res.Fidelity.Score, bare.Fidelity.Score) - } -} - -func TestAnalyze_UnsupportedOptions(t *testing.T) { - tests := []struct { - token string - line string - reason string - }{ - {"-Mh,d,r", "-f1 -Mh,d,r", "httpTamper"}, - {"-O5", "-f1 -O5", "fakeOffsetUnsupported"}, - {"-m3", "-f1 -m3", "noEquivalent"}, - } - for _, tt := range tests { - t.Run(tt.token, func(t *testing.T) { - res := analyze(t, tt.line) - n := noteFor(t, res, tt.token) - if n.Status != StatusUnsupported || n.Reason != tt.reason { - t.Fatalf("got %+v", n) - } - }) - } -} - -func TestAnalyze_UDPProfile(t *testing.T) { - res := analyze(t, "-Ku -a1") - set := res.Sets[0] - if set.UDP.Mode != "fake" || set.UDP.FakeSeqLength != 1 || set.UDP.FilterQUIC != "all" { - t.Fatalf("udp: got mode=%q len=%d quic=%q", set.UDP.Mode, set.UDP.FakeSeqLength, set.UDP.FilterQUIC) - } - if set.Fragmentation.Strategy != config.ConfigNone || set.Faking.SNI { - t.Fatalf("a UDP-only profile must not carry TCP strategies: %q / %v", - set.Fragmentation.Strategy, set.Faking.SNI) - } -} - -func TestAnalyze_ProtoFilterBecomesPortFilter(t *testing.T) { - res := analyze(t, "-Kt,h -H:example.com -s1+s") - if got := res.Sets[0].TCP.DPortFilter; got != "80,443" { - t.Fatalf("dport_filter: got %q", got) - } -} - -func TestAnalyze_PortFilter(t *testing.T) { - res := analyze(t, "-V443-444 -H:example.com -s1+s") - if got := res.Sets[0].TCP.DPortFilter; got != "443-444" { - t.Fatalf("dport_filter: got %q", got) - } -} - -func TestAnalyze_EscalationChain(t *testing.T) { - res := analyze(t, "-H:example.com -s1+s -At -d0+sm -At -f-1") - if len(res.Sets) != 3 { - t.Fatalf("expected 3 sets, got %d", len(res.Sets)) - } - if res.Sets[0].Escalate.To != res.Sets[1].Id { - t.Fatalf("set 0 should escalate to set 1, got %q", res.Sets[0].Escalate.To) - } - if res.Sets[1].Escalate.To != res.Sets[2].Id { - t.Fatalf("set 1 should escalate to set 2, got %q", res.Sets[1].Escalate.To) - } - if res.Sets[2].Escalate.To != "" { - t.Fatalf("last set must not escalate, got %q", res.Sets[2].Escalate.To) - } - for i := 1; i < 3; i++ { - if !res.Sets[i].Enabled { - t.Fatalf("escalation target %d must be enabled", i) - } - if res.Sets[i].TCP.DPortFilter != "" { - t.Fatalf("escalation target %d must not match on ports alone", i) - } - } -} - -func TestAnalyze_AutoNoneIsNotAnEscalation(t *testing.T) { - res := analyze(t, "-Ku -a1 -An -s1+s") - if res.Sets[0].Escalate.To != "" { - t.Fatalf("-An must not create an escalation link, got %q", res.Sets[0].Escalate.To) - } - n := noteFor(t, res, "-An") - if n.Status != StatusMapped || n.Reason != "autoNoneEntrySet" { - t.Fatalf("got %+v", n) - } -} - -func TestAnalyze_UDPOnlyProfileIsFoldedIntoTheEntrySet(t *testing.T) { - res := analyze(t, "-Ku -a1 -An -s1+s -At -d0+sm") - - if len(res.Sets) != 2 { - t.Fatalf("the UDP profile should not become a set of its own, got %d sets", len(res.Sets)) - } - entry := res.Sets[0] - if entry.Fragmentation.Strategy != "tcp" { - t.Fatalf("entry set lost its TCP strategy: %q", entry.Fragmentation.Strategy) - } - if entry.UDP.Mode != "fake" || entry.UDP.FakeSeqLength != 1 || entry.UDP.FilterQUIC != "all" { - t.Fatalf("entry set did not inherit the UDP handling: %+v", entry.UDP) - } - n := noteFor(t, res, "-Ku") - if n.Reason != "udpFoldedIntoSet" { - t.Fatalf("got %+v", n) - } -} - -func TestAnalyze_NoEntrySetIsShadowedByAnother(t *testing.T) { - res, err := Analyze("-Ku -a1 -An -s1+s -At -d0+sm", Options{Domains: []string{"youtube.com"}}) - if err != nil { - t.Fatal(err) - } - claimed := map[string]int{} - for _, s := range res.Sets { - if !s.Enabled { - continue - } - for _, d := range s.Targets.SNIDomains { - claimed[d]++ - } - } - for domain, n := range claimed { - if n > 1 { - t.Fatalf("%q is claimed by %d enabled sets; b4 applies only the first and ignores the rest", domain, n) - } - } -} - -func TestAnalyze_UDPOnlyProfileSurvivesWithoutACarrier(t *testing.T) { - res := analyze(t, "-Ku -a1") - if len(res.Sets) != 1 { - t.Fatalf("expected the UDP profile to stay as its own set, got %d", len(res.Sets)) - } - if res.Sets[0].UDP.FakeSeqLength != 1 { - t.Fatalf("udp: got %+v", res.Sets[0].UDP) - } -} - -func TestAnalyze_UDPProfileWithOwnHostsIsNotFolded(t *testing.T) { - res := analyze(t, "-Ku -H:quic.example.com -a1 -An -H:www.example.com -s1+s") - if len(res.Sets) != 2 { - t.Fatalf("a UDP profile with its own host list is a separate set, got %d", len(res.Sets)) - } -} - func TestAnalyze_PerProfileDomains(t *testing.T) { res, err := Analyze("-An -s1+s -An -d0+sm", Options{ Domains: []string{"fallback.example"}, @@ -366,19 +67,6 @@ func TestAnalyze_PlanDescribesRoles(t *testing.T) { } } -func TestAnalyze_ExplicitVersionOverride(t *testing.T) { - res, err := Analyze("-n example.com -f1", Options{Tool: "byedpi", Version: "0.13"}) - if err != nil { - t.Fatal(err) - } - if res.Version != "0.13" { - t.Fatalf("version: got %q", res.Version) - } - if res.Sets[0].Faking.PayloadDomain != "example.com" { - t.Fatalf("payload_domain: got %q", res.Sets[0].Faking.PayloadDomain) - } -} - func TestAnalyze_DomainsOption(t *testing.T) { res, err := Analyze("-s1+s -At -d0+sm", Options{Domains: []string{"youtube.com"}}) if err != nil { @@ -426,61 +114,3 @@ func TestAnalyze_ToolDetection(t *testing.T) { }) } } - -func TestAnalyze_UserReportedLine(t *testing.T) { - line := "-Ku -a1 -An -o1 -At,r,s -f-1 -At,r,s -d1:11+sm -S -At,r,s " + - "-n https://www.gosuslugi.ru/ -Qr -f1 -d1:11+sm -s1:11+sm -S" - res := analyze(t, line) - - if res.Tool != "byedpi" { - t.Fatalf("tool: got %q", res.Tool) - } - if res.Version != "0.17" { - t.Fatalf("version: got %q, want 0.17 (uses -Q and pos:repeats syntax)", res.Version) - } - if res.VersionInferred { - t.Fatal("version should be detected from markers, not guessed") - } - if len(res.Sets) != 4 { - t.Fatalf("expected 4 sets, got %d", len(res.Sets)) - } - - if res.Sets[0].UDP.FakeSeqLength != 1 { - t.Fatalf("set 0 should carry the folded UDP handling: got %d", res.Sets[0].UDP.FakeSeqLength) - } - if res.Sets[0].Fragmentation.Strategy != "oob" || res.Sets[0].Fragmentation.OOBPosition != 1 { - t.Fatalf("set 0: got %+v", res.Sets[0].Fragmentation) - } - if res.Sets[0].Escalate.To != res.Sets[1].Id || - res.Sets[1].Escalate.To != res.Sets[2].Id || - res.Sets[2].Escalate.To != res.Sets[3].Id { - t.Fatal("expected sets 0 -> 1 -> 2 -> 3 escalation chain") - } - if res.Sets[3].Faking.PayloadDomain != "www.gosuslugi.ru" || !res.Sets[3].Faking.MD5OnFake { - t.Fatalf("set 3 faking: got %+v", res.Sets[3].Faking) - } - if res.Sets[3].Fragmentation.Strategy != "combo" { - t.Fatalf("set 3 strategy: got %q", res.Sets[3].Fragmentation.Strategy) - } - if !res.Sets[3].Fragmentation.Combo.DecoyEnabled { - t.Fatal("a combo profile that also carries -f should enable the decoy") - } - - n := noteFor(t, res, "-n https://www.gosuslugi.ru/") - if n.Status != StatusApproximated || n.Reason != "fakeSNINormalised" { - t.Fatalf("URL passed to --fake-sni should be flagged, got %+v", n) - } - if !hasField(n, "faking.payload_domain") { - t.Fatalf("note should name the field it set, got %+v", n) - } - - var needsTargets bool - for _, w := range res.Warnings { - if w.Code == "needsTargets" { - needsTargets = true - } - } - if !needsTargets { - t.Fatal("a byedpi line with no host filter must warn that targets are required") - } -} diff --git a/src/convert/corpus_test.go b/src/convert/corpus_test.go deleted file mode 100644 index abc3ba61..00000000 --- a/src/convert/corpus_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package convert - -import ( - "testing" - - "github.com/daniellavrushin/b4/config" -) - -var sharedConfigs = []struct { - name string - line string -}{ - { - "gosuslugiEscalation", - "-Ku -a1 -An -o1 -At,r,s -f-1 -At,r,s -d1:11+sm -S -At,r,s " + - "-n https://www.gosuslugi.ru/ -Qr -f1 -d1:11+sm -s1:11+sm -S", - }, - { - "vkLadderTwoProfiles", - "-Ku -a3 -An -Kt,h -n vk.com -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s " + - "-d25+s -s30+s -d35+s -r1+s -S -Mh,d -As -Kt,h -n vk.com -d1 -d3+s -s6+s " + - "-d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -Mh,d", - }, - { - "sevenProfileEscalation", - "-Ku -a1 -An -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s " + - "-d35+s -At,r,s -s1 -q1 -At,r,s -s5 -o25000+s -At,r,s -o1 -d1 -r1+s -t10 " + - "-b1500 -s0+s -d3+s -At,r,s -f-1 -r1+s -At,r,s -s1 -o1+s -s-1", - }, - { - "inlineFakePayloadUDP", - `-Ku -l':\x16\x03\x01\x02\x87\x01\x00\x02\x83\x03\x03\x5f\x15\x63\xcb\x06' ` + - `-a1 -An -s1 -q1 -Y -At -f-1 -r1+s -As`, - }, -} - -func TestCorpus_EveryRecognizedOptionIsReported(t *testing.T) { - for _, tc := range sharedConfigs { - t.Run(tc.name, func(t *testing.T) { - res := analyze(t, tc.line) - reported := map[string]bool{} - for _, n := range res.Notes { - reported[n.Token] = true - } - all, err := loadSpecs() - if err != nil { - t.Fatal(err) - } - table := all[res.Tool].tableFor(res.Version) - for _, tok := range getoptLong(res.Argv, table, false) { - if tok.Spec.Target == "_.ignore" { - continue - } - if !reported[tok.Raw] { - t.Fatalf("option %q produced no entry in the report", tok.Raw) - } - } - }) - } -} - -func TestCorpus_NoUnaccountedOptions(t *testing.T) { - for _, tc := range sharedConfigs { - t.Run(tc.name, func(t *testing.T) { - res := analyze(t, tc.line) - for _, n := range res.Notes { - if n.Reason == "unaccountedOption" { - t.Fatalf("%q fell through every emit rule", n.Token) - } - if n.Status == StatusUnknown || n.Status == StatusInvalid { - t.Fatalf("%q was not understood: %s/%s", n.Token, n.Status, n.Reason) - } - } - }) - } -} - -func TestCorpus_EscalationChainsAreAcyclic(t *testing.T) { - for _, tc := range sharedConfigs { - t.Run(tc.name, func(t *testing.T) { - res := analyze(t, tc.line) - byID := map[string]int{} - for i, s := range res.Sets { - byID[s.Id] = i - } - for i, s := range res.Sets { - if s.Escalate.To == "" { - continue - } - target, ok := byID[s.Escalate.To] - if !ok { - t.Fatalf("set %d escalates to an unknown id %q", i, s.Escalate.To) - } - if target <= i { - t.Fatalf("set %d escalates backwards to %d", i, target) - } - if !res.Sets[target].Enabled { - t.Fatalf("set %d escalates to a disabled set %d", i, target) - } - } - }) - } -} - -func TestAnalyze_UDPProfileStillReportsFakeOptions(t *testing.T) { - res := analyze(t, `-Ku -l':abc' -a1`) - n := noteFor(t, res, "-l:abc") - if n.Status != StatusDegenerate || n.Reason != "requiresFake" { - t.Fatalf("a fake payload in a UDP-only profile must be reported, got %+v", n) - } -} - -func TestAnalyze_ComboHonoursFirstByteSplit(t *testing.T) { - res := analyze(t, "-d1 -s3+s") - set := res.Sets[0] - if set.Fragmentation.Strategy != "combo" { - t.Fatalf("strategy: got %q", set.Fragmentation.Strategy) - } - if !set.Fragmentation.Combo.FirstByteSplit { - t.Fatal("offset 1 should enable combo.first_byte_split") - } - n := noteFor(t, res, "-d1") - if n.Status != StatusMapped || n.Reason != "firstByteMapped" { - t.Fatalf("offset 1 is representable in combo, got %+v", n) - } - if !hasField(n, "fragmentation.combo.first_byte_split") { - t.Fatalf("note should name the field it set, got %+v", n) - } -} - -func TestAnalyze_SplitLadderIsSummarised(t *testing.T) { - res := analyze(t, "-s1 -d3+s -s6+s -d9+s -s12+s -d15+s") - var found *Note - for i := range res.Notes { - if res.Notes[i].Reason == "splitPointsCollapsed" { - found = &res.Notes[i] - } - } - if found == nil { - t.Fatal("a ladder of split points should be summarised once for the profile") - } - if found.Params["count"] != 6 { - t.Fatalf("count: got %v, want 6", found.Params["count"]) - } -} - -func TestAnalyze_ProfileWithoutDesyncIsReported(t *testing.T) { - res := analyze(t, "-s1 -At -f-1 -As") - if len(res.Sets) != 3 { - t.Fatalf("expected 3 sets, got %d", len(res.Sets)) - } - last := res.Sets[2] - if last.Fragmentation.Strategy != "none" || last.Faking.SNI { - t.Fatalf("a trailing -A with no options is a pass-through set, got %+v", last.Fragmentation) - } - var found bool - for _, n := range res.Notes { - if n.Reason == "profileWithoutDesync" && n.Profile == 2 { - found = true - } - } - if !found { - t.Fatal("an empty set must be explained rather than left as a mystery") - } -} - -func TestAnalyze_CompetingFixedPositions(t *testing.T) { - res := analyze(t, "-s5 -s7") - if got := res.Sets[0].Fragmentation.SNIPosition; got != 5 { - t.Fatalf("sni_position: got %d, want 5", got) - } - if n := noteFor(t, res, "-s5"); n.Status != StatusMapped { - t.Fatalf("-s5: got %+v", n) - } - n := noteFor(t, res, "-s7") - if n.Status != StatusApproximated || n.Reason != "fixedPositionIgnored" { - t.Fatalf("a second fixed position cannot be kept, got %+v", n) - } -} - -func TestAnalyze_PositionBeyondRangeIsClamped(t *testing.T) { - res := analyze(t, "-s25000") - if got := res.Sets[0].Fragmentation.SNIPosition; got != maxSNIPosition { - t.Fatalf("sni_position: got %d, want %d", got, maxSNIPosition) - } - n := noteFor(t, res, "-s25000") - if n.Status != StatusApproximated || n.Reason != "positionClamped" { - t.Fatalf("got %+v", n) - } -} - -func TestAnalyze_DroppedOOBDoesNotLeaveItsByteBehind(t *testing.T) { - res := analyze(t, "-s5 -o25000+s") - set := res.Sets[0] - if set.Fragmentation.Strategy == "oob" { - t.Fatal("a plain split should win over oob here") - } - if set.Fragmentation.OOBChar != config.DefaultSetConfig.Fragmentation.OOBChar { - t.Fatalf("oob_char should stay at the b4 default when oob was dropped, got %d", - set.Fragmentation.OOBChar) - } -} diff --git a/src/convert/detect.go b/src/convert/detect.go index a07255bc..9c236862 100644 --- a/src/convert/detect.go +++ b/src/convert/detect.go @@ -5,8 +5,6 @@ import ( "strings" ) -const byedpiV013 = "0.13" - var posHintRe = regexp.MustCompile(`^-{1,2}[A-Za-z-]*=?[+-]?[0-9]+(:[0-9]+)+|\+[shn][emrs]`) type usage struct { diff --git a/src/convert/emit.go b/src/convert/emit.go index ba8a1cfa..7f739805 100644 --- a/src/convert/emit.go +++ b/src/convert/emit.go @@ -10,11 +10,9 @@ import ( ) const ( - byedpiDefaultFakeTTL = 8 - byedpiDefaultOOBByte = 'a' - maxSNIPosition = 50 - maxOOBPosition = 50 - maxTLSRecPosition = 100 + maxSNIPosition = 50 + maxOOBPosition = 50 + maxTLSRecPosition = 100 ) type emitOpts struct { @@ -23,6 +21,7 @@ type emitOpts struct { ProfileDomains map[int][]string ProfileModel string BreakKeys []string + Defaults SpecDefaults } func noteBreakTokens(prof *Profile, ti tokenIndex, notes *noteSet, keys []string, model string) { @@ -95,8 +94,8 @@ func emit(prog *Program, tokens []Token, notes *noteSet, opts emitOpts) []config emitFilters(&set, prof, ti, notes, udpOnly) emitUDP(&set, prof, ti, notes, udpOnly) if !udpOnly { - emitSplits(&set, prof, ti, notes) - emitFake(&set, prog, prof, ti, notes) + emitSplits(&set, prof, ti, notes, opts) + emitFake(&set, prog, prof, ti, notes, opts.Defaults) } else { set.Fragmentation.Strategy = config.ConfigNone set.Faking.SNI = false @@ -111,8 +110,7 @@ func emit(prog *Program, tokens []Token, notes *noteSet, opts emitOpts) []config }) } emitMisc(&set, prog, prof, ti, notes) - emitZapretExtras(&set, prof, ti, notes) - noteDesyncModes(&set, prof, ti, notes) + runToolEmitter(prog.Tool, &set, prof, ti, notes) noteBreakTokens(prof, ti, notes, opts.BreakKeys, opts.ProfileModel) set.Targets.SNIDomains = append(set.Targets.SNIDomains, opts.domainsFor(prof)...) @@ -310,7 +308,7 @@ func emitUDPFake(prof *Profile, ti tokenIndex, notes *noteSet) { } } -func emitSplits(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { +func emitSplits(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet, opts emitOpts) { var plain, disorder, oob, disoob, tlsrec, ipfrag []SplitOp for _, s := range prof.Splits { switch s.Kind { @@ -364,7 +362,9 @@ func emitSplits(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *note set.Fragmentation.Strategy = "oob" pos := clamp(absOffset(oob[0].Pos, 1), 1, maxOOBPosition) set.Fragmentation.OOBPosition = pos - set.Fragmentation.OOBChar = byedpiDefaultOOBByte + if opts.Defaults.OOBByte > 0 { + set.Fragmentation.OOBChar = byte(opts.Defaults.OOBByte) + } ti.each(prof.Index, "oob", func(t Token) { notes.set(t, StatusMapped, "oobMapped", "fragmentation.strategy=oob", "fragmentation.oob_position="+strconv.Itoa(pos)) @@ -520,14 +520,14 @@ func describeSplitMapping(op SplitOp, strategy string, honoursFixed bool, set *c return StatusMapped, "fixedPositionMapped", fields } -func emitFake(set *config.SetConfig, prog *Program, prof *Profile, ti tokenIndex, notes *noteSet) { +func emitFake(set *config.SetConfig, prog *Program, prof *Profile, ti tokenIndex, notes *noteSet, defaults SpecDefaults) { if !prof.Fake.Present { set.Faking.SNI = false noteFakeOptionsUnused(prof, ti, notes) return } set.Faking.SNI = true - applyFooling(set, prof, ti, notes) + applyFooling(set, prof, ti, notes, defaults) if prof.Fake.Repeats > 0 { set.Faking.SNISeqLength = prof.Fake.Repeats if tok, ok := ti.first(prof.Index, "repeats"); ok { @@ -649,124 +649,6 @@ func emitFake(set *config.SetConfig, prog *Program, prof *Profile, ti tokenIndex } } -var zapretDroppedModes = map[string]bool{ - "udplen": true, "tamper": true, "hopbyhop": true, "destopt": true, -} - -func noteDesyncModes(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { - tok, ok := ti.first(prof.Index, "desync") - if !ok || len(prof.DesyncModes) == 0 { - return - } - var fields, dropped []string - if set.Fragmentation.Strategy != config.ConfigNone { - fields = append(fields, "fragmentation.strategy="+set.Fragmentation.Strategy) - } - if set.Faking.SNI { - fields = append(fields, "faking.sni=true") - } - if set.TCP.Desync.Mode != config.ConfigOff { - fields = append(fields, "tcp.desync.mode="+set.TCP.Desync.Mode) - } - if set.TCP.SynFake { - fields = append(fields, "tcp.syn_fake=true") - } - if prof.UDP.Present { - fields = append(fields, "udp.mode="+set.UDP.Mode) - } - for _, m := range prof.DesyncModes { - if zapretDroppedModes[m] { - dropped = append(dropped, m) - } - } - if len(dropped) > 0 { - n := notes.set(tok, StatusUnsupported, "desyncModesDropped", fields...) - n.Params = map[string]any{"dropped": strings.Join(dropped, ", ")} - return - } - if len(fields) == 0 { - notes.set(tok, StatusDegenerate, "desyncModesEmpty") - return - } - notes.set(tok, StatusApproximated, "desyncModesMapped", fields...) -} - -func emitZapretExtras(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { - if prof.Desync.Mode != "" { - set.TCP.Desync.Mode = prof.Desync.Mode - } - if prof.SynFake.Enabled { - set.TCP.SynFake = true - set.TCP.SynFakeLen = prof.SynFake.Len - } - if prof.Duplicate > 0 { - set.TCP.Duplicate.Enabled = true - set.TCP.Duplicate.Count = clamp(prof.Duplicate, 1, 10) - if tok, ok := ti.first(prof.Index, "dup"); ok { - notes.set(tok, StatusMapped, "duplicateMapped", - "tcp.duplicate.enabled=true", "tcp.duplicate.count="+strconv.Itoa(set.TCP.Duplicate.Count)) - } - } - if prof.SeqOvl.Length > 0 { - set.Fragmentation.SeqOverlapLength = prof.SeqOvl.Length - set.Fragmentation.SeqOverlapPattern = seqOvlPattern(prof.SeqOvl.Pattern) - if tok, ok := ti.first(prof.Index, "seqovl"); ok { - notes.set(tok, StatusMapped, "seqOvlMapped", - "fragmentation.seq_overlap_length="+strconv.Itoa(prof.SeqOvl.Length)) - } - if tok, ok := ti.first(prof.Index, "seqovl_pat"); ok { - notes.set(tok, StatusApproximated, "seqOvlPatternMapped", "fragmentation.seq_overlap_pattern") - } - } - if prof.WinSize > 0 { - set.TCP.Win.Mode = "zero" - if tok, ok := ti.first(prof.Index, "wssize"); ok { - notes.set(tok, StatusApproximated, "wsSizeApproximated", "tcp.win.mode=zero") - } - } - if len(prof.Filters.Excluded) > 0 { - if tok, ok := ti.first(prof.Index, "hostlist_excl_dom", "hostlist_exclude"); ok { - notes.set(tok, StatusUnsupported, "excludeListUnsupported") - } - } - if prof.Skip { - set.Enabled = false - if tok, ok := ti.first(prof.Index, "skip"); ok { - notes.set(tok, StatusMapped, "skipMapped", "enabled=false") - } - } -} - -func onlyExtSplit(plain, disorder []SplitOp) bool { - ops := append(append([]SplitOp{}, plain...), disorder...) - if len(ops) != 1 { - return false - } - return ops[0].Pos.Anchor == AnchorSNIExt && ops[0].Pos.Offset == 0 -} - -func plainOrDisorder(plain, disorder []SplitOp) SplitOp { - if len(plain) > 0 { - return plain[0] - } - return disorder[0] -} - -func seqOvlPattern(raw string) []string { - hex := strings.TrimPrefix(strings.TrimPrefix(raw, "0x"), "0X") - if hex == "" || len(hex)%2 != 0 { - return []string{"0x16", "0x03", "0x03", "0x00", "0x00"} - } - out := make([]string, 0, len(hex)/2) - for i := 0; i+1 < len(hex); i += 2 { - if !isHex(hex[i]) || !isHex(hex[i+1]) { - return []string{"0x16", "0x03", "0x03", "0x00", "0x00"} - } - out = append(out, "0x"+hex[i:i+2]) - } - return out -} - func emitMisc(set *config.SetConfig, prog *Program, prof *Profile, ti tokenIndex, notes *noteSet) { if prof.DropSACK { set.TCP.DropSACK = true @@ -902,13 +784,15 @@ var foolingToStrategy = map[string]string{ "ts": "timestamp", } -func applyFooling(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { - ttl := byedpiDefaultFakeTTL - if prof.Fake.TTLSet { - ttl = prof.Fake.TTL +func applyFooling(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet, defaults SpecDefaults) { + switch { + case prof.Fake.TTLSet: + set.Faking.TTL = uint8(clamp(prof.Fake.TTL, 1, 255)) + set.Faking.ApplyTTL = true + case defaults.FakeTTL > 0: + set.Faking.TTL = uint8(clamp(defaults.FakeTTL, 1, 255)) + set.Faking.ApplyTTL = defaults.FakeTTLForced } - set.Faking.TTL = uint8(clamp(ttl, 1, 255)) - set.Faking.ApplyTTL = true set.Faking.Strategy = "ttl" var chosen string diff --git a/src/convert/getopt_test.go b/src/convert/getopt_test.go index 01ade2c2..e73f1994 100644 --- a/src/convert/getopt_test.go +++ b/src/convert/getopt_test.go @@ -71,50 +71,3 @@ func TestGetoptLong_Errors(t *testing.T) { }) } } - -func TestGetoptLong_VersionScopedOptions(t *testing.T) { - v13 := getoptLong([]string{"-Qr"}, testTable(t, "0.13"), false) - if v13[0].Err != "unknown" { - t.Fatalf("expected -Q to be unknown in 0.13, got %+v", v13[0]) - } - v17 := getoptLong([]string{"-Qr"}, testTable(t, "0.17"), false) - if v17[0].Key != "fake_tls_mod" { - t.Fatalf("expected -Q to resolve in 0.17, got %+v", v17[0]) - } - - n13 := getoptLong([]string{"-n", "example.com"}, testTable(t, "0.13"), false) - if n13[0].Key != "tls_sni" { - t.Fatalf("expected -n to be tls_sni in 0.13, got %q", n13[0].Key) - } - n17 := getoptLong([]string{"-n", "example.com"}, testTable(t, "0.17"), false) - if n17[0].Key != "fake_sni" { - t.Fatalf("expected -n to be fake_sni in 0.17, got %q", n17[0].Key) - } -} - -func TestDetectVersion_Markers(t *testing.T) { - all, err := loadSpecs() - if err != nil { - t.Fatal(err) - } - spec := all["byedpi"] - tests := []struct { - name string - argv []string - want string - detected bool - }{ - {"fakeTLSMod", []string{"-Qr"}, "0.17", true}, - {"ipOpt", []string{"-k"}, "0.13", true}, - {"posRepeats", []string{"-d1:11+sm"}, "0.17", true}, - {"ambiguousFallsBackToDefault", []string{"-s1", "-f-1"}, "0.17", false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, detected := detectVersion(spec, tt.argv) - if got != tt.want || detected != tt.detected { - t.Fatalf("got (%s, %v), want (%s, %v)", got, detected, tt.want, tt.detected) - } - }) - } -} diff --git a/src/convert/grammar.go b/src/convert/grammar.go index 732a06a0..0b216e47 100644 --- a/src/convert/grammar.go +++ b/src/convert/grammar.go @@ -30,15 +30,10 @@ var grammars = map[string]grammarFn{ "int": gInt, "str": gStr, "float_sec": gFloatSec, - "cchar": gCChar, - "cdata": gCData, - "hostlist": gHostList, - "iplist": gHostList, "portrange": gPortRange, "range": gRange, "csvfirstchar": gCSVFirstChar, "csvkv": gCSVKeyValue, - "byedpi.pos": gByedpiPos, } func runGrammar(name, raw string, ctx grammarCtx) (Value, error) { @@ -141,47 +136,6 @@ func gCSVKeyValue(raw string, _ grammarCtx) (Value, error) { return Value{List: out, Str: raw}, nil } -func gCChar(raw string, _ grammarCtx) (Value, error) { - dec, err := parseCForm(raw) - if err != nil { - return Value{}, err - } - if len(dec) != 1 { - return Value{}, errors.New("expected exactly one byte") - } - return Value{Byte: dec[0], Str: raw}, nil -} - -func gCData(raw string, _ grammarCtx) (Value, error) { - if strings.HasPrefix(raw, ":") { - dec, err := parseCForm(raw[1:]) - if err != nil { - return Value{}, err - } - return Value{Str: string(dec)}, nil - } - return Value{Ref: raw}, nil -} - -func gHostList(raw string, _ grammarCtx) (Value, error) { - if !strings.HasPrefix(raw, ":") { - return Value{Ref: raw}, nil - } - fields := strings.FieldsFunc(raw[1:], func(r rune) bool { - return r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == ',' || r == ';' - }) - out := make([]string, 0, len(fields)) - for _, f := range fields { - if f != "" { - out = append(out, f) - } - } - if len(out) == 0 { - return Value{}, errors.New("expected at least one entry") - } - return Value{List: out, Str: raw}, nil -} - func parseCForm(s string) ([]byte, error) { out := make([]byte, 0, len(s)) for i := 0; i < len(s); i++ { @@ -247,15 +201,6 @@ func isHex(c byte) bool { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') } -func gByedpiPos(raw string, ctx grammarCtx) (Value, error) { - if ctx.Version == byedpiV013 { - p, err := parsePosV013(raw) - return Value{Pos: p, Str: raw}, err - } - p, err := parsePosV017(raw) - return Value{Pos: p, Str: raw}, err -} - func splitLeadingInt(s string) (int, string, error) { i := 0 if i < len(s) && (s[i] == '+' || s[i] == '-') { @@ -276,86 +221,3 @@ func splitLeadingInt(s string) (int, string, error) { } return int(n), s[i:], nil } - -func parsePosV013(raw string) (Pos, error) { - p := Pos{Raw: raw, Anchor: AnchorAbs, Rel: RelStart} - n, rest, err := splitLeadingInt(raw) - if err != nil { - return p, err - } - p.Offset = n - if rest == "" { - return p, nil - } - if rest[0] != '+' || len(rest) != 2 { - return p, errors.New("expected , +s, +h or +e") - } - switch rest[1] { - case 's': - p.Anchor, p.Rel = AnchorSNI, RelStart - case 'h': - p.Anchor, p.Rel = AnchorHost, RelStart - case 'e': - p.Anchor, p.Rel = AnchorPacket, RelEnd - default: - return p, errors.New("expected +s, +h or +e") - } - return p, nil -} - -func parsePosV017(raw string) (Pos, error) { - p := Pos{Raw: raw, Anchor: AnchorAbs, Rel: RelStart} - n, rest, err := splitLeadingInt(raw) - if err != nil { - return p, err - } - p.Offset = n - for len(rest) > 0 && rest[0] == ':' { - var v int - v, rest, err = splitLeadingInt(rest[1:]) - if err != nil || v < 0 { - return p, errors.New("expected [:repeats[:skip]]") - } - if p.Repeats == 0 { - if v == 0 { - return p, errors.New("repeats must be greater than zero") - } - p.Repeats = v - } else { - p.Skip = v - break - } - } - if rest == "" { - return p, nil - } - if rest[0] != '+' || len(rest) < 2 { - return p, errors.New("expected +s, +h or +n after the offset") - } - switch rest[1] { - case 's': - p.Anchor = AnchorSNI - case 'h': - p.Anchor = AnchorHost - case 'n': - p.Anchor = AnchorPacket - default: - return p, errors.New("expected +s, +h or +n after the offset") - } - if len(rest) > 2 { - switch rest[2] { - case 'e': - p.Rel = RelEnd - case 'm': - p.Rel = RelMid - case 'r': - p.Rel = RelRand - case 's': - p.Rel = RelStart - } - } - if p.Anchor == AnchorPacket && p.Rel == RelStart { - p.Anchor = AnchorAbs - } - return p, nil -} diff --git a/src/convert/grammar_test.go b/src/convert/grammar_test.go index 5f436d02..751030a4 100644 --- a/src/convert/grammar_test.go +++ b/src/convert/grammar_test.go @@ -2,90 +2,6 @@ package convert import "testing" -func TestParsePosV013_Valid(t *testing.T) { - tests := []struct { - name string - in string - offset int - anchor Anchor - rel Rel - }{ - {"plain", "1", 1, AnchorAbs, RelStart}, - {"negative", "-1", -1, AnchorAbs, RelStart}, - {"hex", "0x10", 16, AnchorAbs, RelStart}, - {"sni", "2+s", 2, AnchorSNI, RelStart}, - {"host", "3+h", 3, AnchorHost, RelStart}, - {"end", "4+e", 4, AnchorPacket, RelEnd}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p, err := parsePosV013(tt.in) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.Offset != tt.offset || p.Anchor != tt.anchor || p.Rel != tt.rel { - t.Fatalf("got offset=%d anchor=%s rel=%s, want %d/%s/%s", p.Offset, p.Anchor, p.Rel, tt.offset, tt.anchor, tt.rel) - } - }) - } -} - -func TestParsePosV013_Rejects(t *testing.T) { - for _, in := range []string{"1:11+sm", "1+sm", "1+x", "abc", "1+", "1junk"} { - t.Run(in, func(t *testing.T) { - if _, err := parsePosV013(in); err == nil { - t.Fatalf("expected %q to be rejected", in) - } - }) - } -} - -func TestParsePosV017_Valid(t *testing.T) { - tests := []struct { - name string - in string - offset int - repeats int - skip int - anchor Anchor - rel Rel - }{ - {"plain", "1", 1, 0, 0, AnchorAbs, RelStart}, - {"negative", "-1", -1, 0, 0, AnchorAbs, RelStart}, - {"sniMid", "1:11+sm", 1, 11, 0, AnchorSNI, RelMid}, - {"repeatsSkip", "1:3:5", 1, 3, 5, AnchorAbs, RelStart}, - {"sniStart", "0+s", 0, 0, 0, AnchorSNI, RelStart}, - {"sniEnd", "0+se", 0, 0, 0, AnchorSNI, RelEnd}, - {"sniRand", "0+sr", 0, 0, 0, AnchorSNI, RelRand}, - {"hostMid", "2+hm", 2, 0, 0, AnchorHost, RelMid}, - {"packetMid", "0+nm", 0, 0, 0, AnchorPacket, RelMid}, - {"nullBase", "5+n", 5, 0, 0, AnchorAbs, RelStart}, - {"unknownSecondCharIgnored", "5+sX", 5, 0, 0, AnchorSNI, RelStart}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p, err := parsePosV017(tt.in) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if p.Offset != tt.offset || p.Repeats != tt.repeats || p.Skip != tt.skip || p.Anchor != tt.anchor || p.Rel != tt.rel { - t.Fatalf("got %+v, want offset=%d repeats=%d skip=%d anchor=%s rel=%s", - p, tt.offset, tt.repeats, tt.skip, tt.anchor, tt.rel) - } - }) - } -} - -func TestParsePosV017_Rejects(t *testing.T) { - for _, in := range []string{"1:0", "abc", "1+x", "1+"} { - t.Run(in, func(t *testing.T) { - if _, err := parsePosV017(in); err == nil { - t.Fatalf("expected %q to be rejected", in) - } - }) - } -} - func TestParseCForm_Escapes(t *testing.T) { tests := []struct { name string @@ -138,23 +54,6 @@ func TestGCSVKeyValue_CapturesMSize(t *testing.T) { } } -func TestGHostList_InlineVsFile(t *testing.T) { - inline, err := gHostList(":a.com b.com,c.com", grammarCtx{}) - if err != nil { - t.Fatal(err) - } - if len(inline.List) != 3 { - t.Fatalf("got %v", inline.List) - } - file, err := gHostList("/etc/byedpi/hosts.txt", grammarCtx{}) - if err != nil { - t.Fatal(err) - } - if file.Ref != "/etc/byedpi/hosts.txt" { - t.Fatalf("got ref %q", file.Ref) - } -} - func TestSanitizeHost(t *testing.T) { tests := []struct{ in, want string }{ {"https://www.gosuslugi.ru/", "www.gosuslugi.ru"}, diff --git a/src/convert/normalize.go b/src/convert/normalize.go deleted file mode 100644 index dc11f871..00000000 --- a/src/convert/normalize.go +++ /dev/null @@ -1,74 +0,0 @@ -package convert - -var normalizers = map[string]func(*Program, []Token, *noteSet){ - "zapret": normalizeZapret, -} - -func runNormalizer(name string, prog *Program, tokens []Token, notes *noteSet) { - if fn, ok := normalizers[name]; ok { - fn(prog, tokens, notes) - } -} - -func normalizeZapret(prog *Program, _ []Token, notes *noteSet) { - for _, prof := range prog.Profiles { - normalizeZapretProfile(prof, notes) - promoteUDPFake(prof) - } -} - -func promoteUDPFake(prof *Profile) { - if !prof.UDPOnly() { - return - } - prof.UDP.Present = prof.Fake.Present - prof.UDP.Repeats = prof.Fake.Repeats - prof.UDP.QUICRef = prof.Fake.QUICRef - prof.UDP.TTL = prof.Fake.TTL - prof.UDP.TTLSet = prof.Fake.TTLSet - prof.UDP.Ports = append(prof.UDP.Ports, prof.Filters.UDPPorts...) -} - -func normalizeZapretProfile(prof *Profile, notes *noteSet) { - positions := prof.SplitPositions - token := prof.SplitPosToken - if len(positions) == 0 { - positions = []Pos{{Raw: "1", Offset: 1, Anchor: AnchorAbs, Rel: RelStart}} - token = prof.DesyncToken - } - - for _, mode := range prof.DesyncModes { - switch mode { - case "fake", "fakeknown": - prof.Fake.Present = true - case "rst", "rstack": - prof.Desync.Mode = "rst" - case "synack": - prof.SynFake.Enabled = true - case "syndata": - prof.SynFake.Enabled = true - prof.SynFake.Len = 1 - case "multisplit": - appendSplits(prof, SplitPlain, positions, token) - case "multidisorder": - appendSplits(prof, SplitDisorder, positions, token) - case "fakedsplit": - prof.Fake.Present = true - appendSplits(prof, SplitPlain, positions[:1], token) - case "fakeddisorder": - prof.Fake.Present = true - appendSplits(prof, SplitDisorder, positions[:1], token) - case "hostfakesplit": - prof.Fake.Present = true - appendSplits(prof, SplitPlain, positions[:1], token) - case "ipfrag1", "ipfrag2": - appendSplits(prof, SplitIPFrag, positions[:1], token) - } - } -} - -func appendSplits(prof *Profile, kind SplitKind, positions []Pos, token int) { - for _, p := range positions { - prof.Splits = append(prof.Splits, SplitOp{Kind: kind, Pos: p, Token: token}) - } -} diff --git a/src/convert/rules/byedpi.json b/src/convert/rules/byedpi.json index f988fb1e..990657bf 100644 --- a/src/convert/rules/byedpi.json +++ b/src/convert/rules/byedpi.json @@ -3,101 +3,661 @@ "label": "byedpi", "style": "getopt_long", "homepage": "https://github.com/hufrea/byedpi", + "defaults": { + "fake_ttl": 8, + "fake_ttl_forced": true, + "oob_byte": 97 + }, "detect": { - "markers": ["ciadpi", "byedpi"], - "signature": ["-K", "-A", "--proto", "--auto", "--disoob", "--tlsrec", "--oob-data"], - "reject": ["--dpi-desync", "--new", "--qnum", "--hostlist", "--filter-tcp"] + "markers": [ + "ciadpi", + "byedpi" + ], + "signature": [ + "-K", + "-A", + "--proto", + "--auto", + "--disoob", + "--tlsrec", + "--oob-data" + ], + "reject": [ + "--dpi-desync", + "--new", + "--qnum", + "--hostlist", + "--filter-tcp" + ] }, "versions": [ { "id": "0.13", "label": "0.13", - "markers": ["-k", "--ip-opt"] + "markers": [ + "-k", + "--ip-opt" + ] }, { "id": "0.17", "label": "0.15 - 0.17", "markers": [ - "-Q", "--fake-tls-mod", - "-L", "--auto-mode", - "-R", "--round", - "-j", "--ipset", - "-m", "--tlsminor", - "-B", "--copy", - "-C", "--connect-to", - "-y", "--cache-dump", - "-D", "--daemon", - "-E", "--transparent", - "-G", "--http-connect", - "-Z", "--wait-send" + "-Q", + "--fake-tls-mod", + "-L", + "--auto-mode", + "-R", + "--round", + "-j", + "--ipset", + "-m", + "--tlsminor", + "-B", + "--copy", + "-C", + "--connect-to", + "-y", + "--cache-dump", + "-D", + "--daemon", + "-E", + "--transparent", + "-G", + "--http-connect", + "-Z", + "--wait-send" ], "pos_hint": true, "default": true } ], - "ambiguous": ["-w", "-W", "-n", "-O"], - "profile_break": ["auto"], + "ambiguous": [ + "-w", + "-W", + "-n", + "-O" + ], + "profile_break": [ + "auto" + ], "options": [ - { "key": "no_domain", "short": "N", "long": "no-domain", "arg": "none", "scope": "global", "target": "_.na", "note": "resolverBehaviour" }, - { "key": "no_ipv6", "short": "X", "long": "no-ipv6", "arg": "none", "scope": "global", "target": "global.no_ipv6" }, - { "key": "no_udp", "short": "U", "long": "no-udp", "arg": "none", "scope": "global", "target": "global.no_udp" }, - { "key": "help", "short": "h", "long": "help", "arg": "none", "scope": "global", "target": "_.ignore" }, - { "key": "show_version", "short": "v", "long": "version", "arg": "none", "scope": "global", "target": "_.ignore" }, - - { "key": "listen_ip", "short": "i", "long": "ip", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "proxyRuntime" }, - { "key": "listen_port", "short": "p", "long": "port", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "proxyRuntime" }, - { "key": "conn_ip", "short": "I", "long": "conn-ip", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "proxyRuntime" }, - { "key": "buf_size", "short": "b", "long": "buf-size", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "proxyRuntime" }, - { "key": "max_conn", "short": "c", "long": "max-conn", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "proxyRuntime" }, - { "key": "debug", "short": "x", "long": "debug", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "proxyRuntime" }, - { "key": "tfo", "short": "F", "long": "tfo", "arg": "none", "scope": "global", "target": "_.na", "note": "proxyRuntime" }, - { "key": "protect_path", "short": "P", "long": "protect-path", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "proxyRuntime" }, - - { "key": "auto", "short": "A", "long": "auto", "arg": "required", "scope": "break", "grammar": "csvfirstchar", "target": "trigger" }, - { "key": "cache_ttl", "short": "u", "long": "cache-ttl", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "autoRetryTiming" }, - { "key": "timeout", "short": "T", "long": "timeout", "arg": "required", "scope": "global", "grammar": "float_sec", "target": "_.na", "note": "autoRetryTiming" }, - { "key": "def_ttl", "short": "g", "long": "def-ttl", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "globalTTL" }, - - { "key": "proto", "short": "K", "long": "proto", "arg": "required", "scope": "profile", "grammar": "csvfirstchar", "target": "filters.proto" }, - { "key": "hosts", "short": "H", "long": "hosts", "arg": "required", "scope": "profile", "grammar": "hostlist", "target": "filters.hosts" }, - { "key": "pf", "short": "V", "long": "pf", "arg": "required", "scope": "profile", "grammar": "portrange", "target": "filters.ports" }, - - { "key": "split", "short": "s", "long": "split", "arg": "required", "scope": "profile", "grammar": "byedpi.pos", "target": "splits[]", "const": { "kind": "split" } }, - { "key": "disorder", "short": "d", "long": "disorder", "arg": "required", "scope": "profile", "grammar": "byedpi.pos", "target": "splits[]", "const": { "kind": "disorder" } }, - { "key": "oob", "short": "o", "long": "oob", "arg": "required", "scope": "profile", "grammar": "byedpi.pos", "target": "splits[]", "const": { "kind": "oob" } }, - { "key": "disoob", "short": "q", "long": "disoob", "arg": "required", "scope": "profile", "grammar": "byedpi.pos", "target": "splits[]", "const": { "kind": "disoob" } }, - { "key": "fake", "short": "f", "long": "fake", "arg": "required", "scope": "profile", "grammar": "byedpi.pos", "target": "splits[]", "const": { "kind": "fake" } }, - { "key": "tlsrec", "short": "r", "long": "tlsrec", "arg": "required", "scope": "profile", "grammar": "byedpi.pos", "target": "splits[]", "const": { "kind": "tlsrec" } }, - - { "key": "ttl", "short": "t", "long": "ttl", "arg": "required", "scope": "profile", "grammar": "int", "target": "fake.ttl" }, - { "key": "md5sig", "short": "S", "long": "md5sig", "arg": "none", "scope": "profile", "target": "fake.md5sig" }, - { "key": "fake_data", "short": "l", "long": "fake-data", "arg": "required", "scope": "profile", "grammar": "cdata", "target": "fake.data" }, - { "key": "oob_data", "short": "e", "long": "oob-data", "arg": "required", "scope": "profile", "grammar": "cchar", "target": "profile.oob_byte" }, - { "key": "mod_http", "short": "M", "long": "mod-http", "arg": "required", "scope": "profile", "grammar": "csvfirstchar", "target": "profile.http_mod" }, - { "key": "udp_fake", "short": "a", "long": "udp-fake", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.udp_fake_count" }, - { "key": "drop_sack", "short": "Y", "long": "drop-sack", "arg": "none", "scope": "profile", "target": "profile.drop_sack" }, - - { "key": "ip_opt", "short": "k", "long": "ip-opt", "arg": "optional", "scope": "profile", "grammar": "cdata", "target": "fake.ip_opt", "versions": ["0.13"] }, - { "key": "tls_sni", "short": "n", "long": "tls-sni", "arg": "required", "scope": "global", "grammar": "str", "target": "global.fake_sni", "versions": ["0.13"] }, - { "key": "fake_offset", "short": "O", "long": "fake-offset", "arg": "required", "scope": "profile", "grammar": "int", "target": "fake.offset", "versions": ["0.13"] }, - { "key": "delay", "short": "w", "long": "delay", "arg": "required", "scope": "global", "grammar": "int", "target": "global.delay", "versions": ["0.13"] }, - { "key": "not_wait_send", "short": "W", "long": "not-wait-send", "arg": "none", "scope": "global", "target": "_.na", "note": "proxyRuntime", "versions": ["0.13"] }, - - { "key": "fake_sni", "short": "n", "long": "fake-sni", "arg": "required", "scope": "profile", "grammar": "str", "target": "fake.sni[]", "versions": ["0.17"] }, - { "key": "fake_offset_pos", "short": "O", "long": "fake-offset", "arg": "required", "scope": "profile", "grammar": "byedpi.pos", "target": "fake.offset_pos", "versions": ["0.17"] }, - { "key": "fake_tls_mod", "short": "Q", "long": "fake-tls-mod", "arg": "required", "scope": "profile", "grammar": "csvkv", "target": "fake.tls_mod", "versions": ["0.17"] }, - { "key": "tls_minor", "short": "m", "long": "tlsminor", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.tls_minor","versions": ["0.17"] }, - { "key": "round", "short": "R", "long": "round", "arg": "required", "scope": "profile", "grammar": "range", "target": "profile.round", "versions": ["0.17"] }, - { "key": "ipset", "short": "j", "long": "ipset", "arg": "required", "scope": "profile", "grammar": "iplist", "target": "filters.ips", "versions": ["0.17"] }, - { "key": "connect_to", "short": "C", "long": "connect-to", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "noEquivalent", "versions": ["0.17"] }, - { "key": "copy", "short": "B", "long": "copy", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "profileCopy", "versions": ["0.17"] }, - { "key": "auto_mode", "short": "L", "long": "auto-mode", "arg": "required", "scope": "global", "grammar": "csvfirstchar", "target": "_.na", "note": "autoRetryTiming", "versions": ["0.17"] }, - { "key": "pidfile", "short": "w", "long": "pidfile", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "processControl", "versions": ["0.17"] }, - { "key": "await_int", "short": "W", "long": "await-int", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "proxyRuntime", "versions": ["0.17"] }, - { "key": "cache_dump", "short": "y", "long": "cache-dump", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "processControl", "versions": ["0.17"] }, - { "key": "daemon", "short": "D", "long": "daemon", "arg": "none", "scope": "global", "target": "_.na", "note": "processControl", "versions": ["0.17"] }, - { "key": "transparent", "short": "E", "long": "transparent", "arg": "none", "scope": "global", "target": "_.na", "note": "proxyRuntime", "versions": ["0.17"] }, - { "key": "http_connect", "short": "G", "long": "http-connect", "arg": "none", "scope": "global", "target": "_.na", "note": "proxyRuntime", "versions": ["0.17"] }, - { "key": "wait_send", "short": "Z", "long": "wait-send", "arg": "none", "scope": "global", "target": "_.na", "note": "proxyRuntime", "versions": ["0.17"] } + { + "key": "no_domain", + "short": "N", + "long": "no-domain", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "resolverBehaviour" + }, + { + "key": "no_ipv6", + "short": "X", + "long": "no-ipv6", + "arg": "none", + "scope": "global", + "target": "global.no_ipv6" + }, + { + "key": "no_udp", + "short": "U", + "long": "no-udp", + "arg": "none", + "scope": "global", + "target": "global.no_udp" + }, + { + "key": "help", + "short": "h", + "long": "help", + "arg": "none", + "scope": "global", + "target": "_.ignore" + }, + { + "key": "show_version", + "short": "v", + "long": "version", + "arg": "none", + "scope": "global", + "target": "_.ignore" + }, + { + "key": "listen_ip", + "short": "i", + "long": "ip", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "listen_port", + "short": "p", + "long": "port", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "conn_ip", + "short": "I", + "long": "conn-ip", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "buf_size", + "short": "b", + "long": "buf-size", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "max_conn", + "short": "c", + "long": "max-conn", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "debug", + "short": "x", + "long": "debug", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "tfo", + "short": "F", + "long": "tfo", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "protect_path", + "short": "P", + "long": "protect-path", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "proxyRuntime" + }, + { + "key": "auto", + "short": "A", + "long": "auto", + "arg": "required", + "scope": "break", + "grammar": "csvfirstchar", + "target": "trigger" + }, + { + "key": "cache_ttl", + "short": "u", + "long": "cache-ttl", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "autoRetryTiming" + }, + { + "key": "timeout", + "short": "T", + "long": "timeout", + "arg": "required", + "scope": "global", + "grammar": "float_sec", + "target": "_.na", + "note": "autoRetryTiming" + }, + { + "key": "def_ttl", + "short": "g", + "long": "def-ttl", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "globalTTL" + }, + { + "key": "proto", + "short": "K", + "long": "proto", + "arg": "required", + "scope": "profile", + "grammar": "csvfirstchar", + "target": "filters.proto" + }, + { + "key": "hosts", + "short": "H", + "long": "hosts", + "arg": "required", + "scope": "profile", + "grammar": "hostlist", + "target": "filters.hosts" + }, + { + "key": "pf", + "short": "V", + "long": "pf", + "arg": "required", + "scope": "profile", + "grammar": "portrange", + "target": "filters.ports" + }, + { + "key": "split", + "short": "s", + "long": "split", + "arg": "required", + "scope": "profile", + "grammar": "byedpi.pos", + "target": "splits[]", + "const": { + "kind": "split" + } + }, + { + "key": "disorder", + "short": "d", + "long": "disorder", + "arg": "required", + "scope": "profile", + "grammar": "byedpi.pos", + "target": "splits[]", + "const": { + "kind": "disorder" + } + }, + { + "key": "oob", + "short": "o", + "long": "oob", + "arg": "required", + "scope": "profile", + "grammar": "byedpi.pos", + "target": "splits[]", + "const": { + "kind": "oob" + } + }, + { + "key": "disoob", + "short": "q", + "long": "disoob", + "arg": "required", + "scope": "profile", + "grammar": "byedpi.pos", + "target": "splits[]", + "const": { + "kind": "disoob" + } + }, + { + "key": "fake", + "short": "f", + "long": "fake", + "arg": "required", + "scope": "profile", + "grammar": "byedpi.pos", + "target": "splits[]", + "const": { + "kind": "fake" + } + }, + { + "key": "tlsrec", + "short": "r", + "long": "tlsrec", + "arg": "required", + "scope": "profile", + "grammar": "byedpi.pos", + "target": "splits[]", + "const": { + "kind": "tlsrec" + } + }, + { + "key": "ttl", + "short": "t", + "long": "ttl", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "fake.ttl" + }, + { + "key": "md5sig", + "short": "S", + "long": "md5sig", + "arg": "none", + "scope": "profile", + "target": "fake.md5sig" + }, + { + "key": "fake_data", + "short": "l", + "long": "fake-data", + "arg": "required", + "scope": "profile", + "grammar": "cdata", + "target": "fake.data" + }, + { + "key": "oob_data", + "short": "e", + "long": "oob-data", + "arg": "required", + "scope": "profile", + "grammar": "cchar", + "target": "profile.oob_byte" + }, + { + "key": "mod_http", + "short": "M", + "long": "mod-http", + "arg": "required", + "scope": "profile", + "grammar": "csvfirstchar", + "target": "profile.http_mod" + }, + { + "key": "udp_fake", + "short": "a", + "long": "udp-fake", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.udp_fake_count" + }, + { + "key": "drop_sack", + "short": "Y", + "long": "drop-sack", + "arg": "none", + "scope": "profile", + "target": "profile.drop_sack" + }, + { + "key": "ip_opt", + "short": "k", + "long": "ip-opt", + "arg": "optional", + "scope": "profile", + "grammar": "cdata", + "target": "fake.ip_opt", + "versions": [ + "0.13" + ] + }, + { + "key": "tls_sni", + "short": "n", + "long": "tls-sni", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "global.fake_sni", + "versions": [ + "0.13" + ] + }, + { + "key": "fake_offset", + "short": "O", + "long": "fake-offset", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "fake.offset", + "versions": [ + "0.13" + ] + }, + { + "key": "delay", + "short": "w", + "long": "delay", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "global.delay", + "versions": [ + "0.13" + ] + }, + { + "key": "not_wait_send", + "short": "W", + "long": "not-wait-send", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "proxyRuntime", + "versions": [ + "0.13" + ] + }, + { + "key": "fake_sni", + "short": "n", + "long": "fake-sni", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "fake.sni[]", + "versions": [ + "0.17" + ] + }, + { + "key": "fake_offset_pos", + "short": "O", + "long": "fake-offset", + "arg": "required", + "scope": "profile", + "grammar": "byedpi.pos", + "target": "fake.offset_pos", + "versions": [ + "0.17" + ] + }, + { + "key": "fake_tls_mod", + "short": "Q", + "long": "fake-tls-mod", + "arg": "required", + "scope": "profile", + "grammar": "csvkv", + "target": "fake.tls_mod", + "versions": [ + "0.17" + ] + }, + { + "key": "tls_minor", + "short": "m", + "long": "tlsminor", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.tls_minor", + "versions": [ + "0.17" + ] + }, + { + "key": "round", + "short": "R", + "long": "round", + "arg": "required", + "scope": "profile", + "grammar": "range", + "target": "profile.round", + "versions": [ + "0.17" + ] + }, + { + "key": "ipset", + "short": "j", + "long": "ipset", + "arg": "required", + "scope": "profile", + "grammar": "iplist", + "target": "filters.ips", + "versions": [ + "0.17" + ] + }, + { + "key": "connect_to", + "short": "C", + "long": "connect-to", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "noEquivalent", + "versions": [ + "0.17" + ] + }, + { + "key": "copy", + "short": "B", + "long": "copy", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "profileCopy", + "versions": [ + "0.17" + ] + }, + { + "key": "auto_mode", + "short": "L", + "long": "auto-mode", + "arg": "required", + "scope": "global", + "grammar": "csvfirstchar", + "target": "_.na", + "note": "autoRetryTiming", + "versions": [ + "0.17" + ] + }, + { + "key": "pidfile", + "short": "w", + "long": "pidfile", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "processControl", + "versions": [ + "0.17" + ] + }, + { + "key": "await_int", + "short": "W", + "long": "await-int", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "proxyRuntime", + "versions": [ + "0.17" + ] + }, + { + "key": "cache_dump", + "short": "y", + "long": "cache-dump", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "processControl", + "versions": [ + "0.17" + ] + }, + { + "key": "daemon", + "short": "D", + "long": "daemon", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "processControl", + "versions": [ + "0.17" + ] + }, + { + "key": "transparent", + "short": "E", + "long": "transparent", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "proxyRuntime", + "versions": [ + "0.17" + ] + }, + { + "key": "http_connect", + "short": "G", + "long": "http-connect", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "proxyRuntime", + "versions": [ + "0.17" + ] + }, + { + "key": "wait_send", + "short": "Z", + "long": "wait-send", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "proxyRuntime", + "versions": [ + "0.17" + ] + } ] } diff --git a/src/convert/rules/zapret.json b/src/convert/rules/zapret.json index 54134fdd..7352ac04 100644 --- a/src/convert/rules/zapret.json +++ b/src/convert/rules/zapret.json @@ -3,10 +3,18 @@ "label": "zapret (nfqws)", "style": "long_only", "homepage": "https://github.com/bol-van/zapret", + "defaults": { + "fake_ttl": 7, + "fake_ttl_forced": true + }, "normalize": "zapret", "profile_model": "alternative", "detect": { - "markers": ["nfqws", "winws", "zapret"], + "markers": [ + "nfqws", + "winws", + "zapret" + ], "signature": [ "--dpi-desync", "--new", @@ -17,142 +25,1064 @@ "--filter-udp", "--dpi-desync-split-pos" ], - "reject": ["--disoob", "--tlsrec", "--oob-data", "--fake-sni", "--mod-http"] + "reject": [ + "--disoob", + "--tlsrec", + "--oob-data", + "--fake-sni", + "--mod-http" + ] }, - "versions": [{ "id": "master", "label": "current", "default": true }], - "profile_break": ["new"], + "versions": [ + { + "id": "master", + "label": "current", + "default": true + } + ], + "profile_break": [ + "new" + ], "options": [ - { "key": "new", "long": "new", "arg": "none", "scope": "break", "target": "trigger" }, - { "key": "skip", "long": "skip", "arg": "none", "scope": "profile", "target": "profile.skip" }, - - { "key": "desync", "long": "dpi-desync", "arg": "required", "scope": "profile", "grammar": "zapret.desync", "target": "desync.modes" }, - { "key": "split_pos", "long": "dpi-desync-split-pos", "arg": "required", "scope": "profile", "grammar": "zapret.splitpos", "target": "splits.positions" }, - { "key": "fooling", "long": "dpi-desync-fooling", "arg": "required", "scope": "profile", "grammar": "zapret.fooling", "target": "fake.fooling" }, - { "key": "desync_ttl", "long": "dpi-desync-ttl", "arg": "required", "scope": "profile", "grammar": "int", "target": "fake.ttl" }, - { "key": "repeats", "long": "dpi-desync-repeats", "arg": "required", "scope": "profile", "grammar": "int", "target": "fake.repeats" }, - { "key": "badseq_inc", "long": "dpi-desync-badseq-increment", "arg": "required", "scope": "profile", "grammar": "int", "target": "fake.seq_increment" }, - { "key": "ts_inc", "long": "dpi-desync-ts-increment", "arg": "required", "scope": "profile", "grammar": "int", "target": "fake.ts_increment" }, - { "key": "seqovl", "long": "dpi-desync-split-seqovl", "arg": "required", "scope": "profile", "grammar": "zapret.pos", "target": "profile.seqovl_len" }, - { "key": "seqovl_pat", "long": "dpi-desync-split-seqovl-pattern", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.seqovl_pattern" }, - - { "key": "fake_tls", "long": "dpi-desync-fake-tls", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "fake.blob" }, - { "key": "fake_tls_mod", "long": "dpi-desync-fake-tls-mod", "arg": "required", "scope": "profile", "grammar": "zapret.modlist", "target": "fake.tls_sni" }, - { "key": "fake_quic", "long": "dpi-desync-fake-quic", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "fake.quic" }, - { "key": "fake_http", "long": "dpi-desync-fake-http", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "httpFakePayload" }, - { "key": "fake_unknown", "long": "dpi-desync-fake-unknown", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fake_syndata", "long": "dpi-desync-fake-syndata", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fake_wg", "long": "dpi-desync-fake-wireguard", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fake_dht", "long": "dpi-desync-fake-dht", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fake_discord", "long": "dpi-desync-fake-discord", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fake_stun", "long": "dpi-desync-fake-stun", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fake_unk_udp", "long": "dpi-desync-fake-unknown-udp", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - - { "key": "autottl", "long": "dpi-desync-autottl", "arg": "required", "scope": "profile", "grammar": "zapret.autottl", "target": "profile.unsupported", "note": "autoTTLUnsupported" }, - { "key": "autottl6", "long": "dpi-desync-autottl6", "arg": "required", "scope": "profile", "grammar": "zapret.autottl", "target": "profile.unsupported", "note": "autoTTLUnsupported" }, - { "key": "desync_ttl6", "long": "dpi-desync-ttl6", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "ipv6OnlyKnob" }, - { "key": "desync_start", "long": "dpi-desync-start", "arg": "required", "scope": "profile", "grammar": "zapret.startcutoff", "target": "profile.unsupported", "note": "packetWindowUnsupported" }, - { "key": "desync_cutoff", "long": "dpi-desync-cutoff", "arg": "required", "scope": "profile", "grammar": "zapret.startcutoff", "target": "profile.unsupported", "note": "packetWindowUnsupported" }, - { "key": "flags_set", "long": "dpi-desync-tcp-flags-set", "arg": "required", "scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "flags_unset", "long": "dpi-desync-tcp-flags-unset", "arg": "required", "scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fake_tcp_mod", "long": "dpi-desync-fake-tcp-mod", "arg": "required", "scope": "profile", "grammar": "zapret.modlist", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fakedsplit_mod","long": "dpi-desync-fakedsplit-mod","arg": "required", "scope": "profile", "grammar": "zapret.modlist", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "fakedsplit_pat","long": "dpi-desync-fakedsplit-pattern","arg": "required","scope": "profile","grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "hfs_midhost", "long": "dpi-desync-hostfakesplit-midhost", "arg": "required", "scope": "profile", "grammar": "zapret.pos", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "hfs_mod", "long": "dpi-desync-hostfakesplit-mod", "arg": "required", "scope": "profile", "grammar": "zapret.modlist", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "ipfrag_tcp", "long": "dpi-desync-ipfrag-pos-tcp", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "ipFragPosition" }, - { "key": "ipfrag_udp", "long": "dpi-desync-ipfrag-pos-udp", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "ipFragPosition" }, - { "key": "udplen_inc", "long": "dpi-desync-udplen-increment", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "udplen_pat", "long": "dpi-desync-udplen-pattern", "arg": "required", "scope": "profile", "grammar": "zapret.blob", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "any_protocol", "long": "dpi-desync-any-protocol", "arg": "optional", "scope": "profile", "grammar": "bool01", "target": "profile.unsupported", "note": "anyProtocol" }, - { "key": "skip_nosni", "long": "dpi-desync-skip-nosni", "arg": "optional", "scope": "profile", "grammar": "bool01", "target": "_.na", "note": "engineDetail" }, - { "key": "desync_fwmark", "long": "dpi-desync-fwmark", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "engineDetail" }, - { "key": "sockarg", "long": "dpi-desync-sockarg", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "engineDetail" }, - - { "key": "dup", "long": "dup", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.duplicate" }, - { "key": "dup_replace", "long": "dup-replace", "arg": "optional", "scope": "profile", "grammar": "bool01", "target": "_.na", "note": "engineDetail" }, - { "key": "dup_ttl", "long": "dup-ttl", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "dupKnobUnsupported" }, - { "key": "dup_ttl6", "long": "dup-ttl6", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "dupKnobUnsupported" }, - { "key": "dup_autottl", "long": "dup-autottl", "arg": "required", "scope": "profile", "grammar": "zapret.autottl", "target": "profile.unsupported", "note": "autoTTLUnsupported" }, - { "key": "dup_autottl6", "long": "dup-autottl6", "arg": "required", "scope": "profile", "grammar": "zapret.autottl", "target": "profile.unsupported", "note": "autoTTLUnsupported" }, - { "key": "dup_fooling", "long": "dup-fooling", "arg": "required", "scope": "profile", "grammar": "zapret.fooling", "target": "profile.unsupported", "note": "dupKnobUnsupported" }, - { "key": "dup_ts_inc", "long": "dup-ts-increment", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "dupKnobUnsupported" }, - { "key": "dup_badseq", "long": "dup-badseq-increment", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "dupKnobUnsupported" }, - { "key": "dup_badack", "long": "dup-badack-increment", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "dupKnobUnsupported" }, - { "key": "dup_ip_id", "long": "dup-ip-id", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "dupKnobUnsupported" }, - { "key": "dup_start", "long": "dup-start", "arg": "required", "scope": "profile", "grammar": "zapret.startcutoff", "target": "profile.unsupported", "note": "packetWindowUnsupported" }, - { "key": "dup_cutoff", "long": "dup-cutoff", "arg": "required", "scope": "profile", "grammar": "zapret.startcutoff", "target": "profile.unsupported", "note": "packetWindowUnsupported" }, - { "key": "dup_flags_set", "long": "dup-tcp-flags-set", "arg": "required", "scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "dup_flags_unset","long": "dup-tcp-flags-unset","arg": "required", "scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "noEquivalent" }, - - { "key": "wssize", "long": "wssize", "arg": "required", "scope": "profile", "grammar": "zapret.wsize", "target": "profile.win_size" }, - { "key": "wsize", "long": "wsize", "arg": "required", "scope": "profile", "grammar": "zapret.wsize", "target": "profile.unsupported", "note": "obsoleteOption" }, - { "key": "wssize_cutoff", "long": "wssize-cutoff", "arg": "required", "scope": "profile", "grammar": "zapret.startcutoff", "target": "profile.unsupported", "note": "packetWindowUnsupported" }, - { "key": "wssize_forced", "long": "wssize-forced-cutoff", "arg": "optional", "scope": "profile", "grammar": "bool01", "target": "_.na", "note": "engineDetail" }, - - { "key": "orig_ttl", "long": "orig-ttl", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "origPacketTTL" }, - { "key": "orig_ttl6", "long": "orig-ttl6", "arg": "required", "scope": "profile", "grammar": "int", "target": "profile.unsupported", "note": "origPacketTTL" }, - { "key": "orig_autottl", "long": "orig-autottl", "arg": "required", "scope": "profile", "grammar": "zapret.autottl", "target": "profile.unsupported", "note": "origPacketTTL" }, - { "key": "orig_autottl6", "long": "orig-autottl6", "arg": "required", "scope": "profile", "grammar": "zapret.autottl", "target": "profile.unsupported", "note": "origPacketTTL" }, - { "key": "orig_flags_set","long": "orig-tcp-flags-set", "arg": "required", "scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "orig_flags_unset","long": "orig-tcp-flags-unset","arg": "required","scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "orig_mod_start","long": "orig-mod-start", "arg": "required", "scope": "profile", "grammar": "zapret.startcutoff", "target": "profile.unsupported", "note": "packetWindowUnsupported" }, - { "key": "orig_mod_cutoff","long": "orig-mod-cutoff","arg": "required","scope": "profile", "grammar": "zapret.startcutoff", "target": "profile.unsupported", "note": "packetWindowUnsupported" }, - { "key": "synack_split", "long": "synack-split", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "noEquivalent" }, - { "key": "ip_id", "long": "ip-id", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "noEquivalent" }, - - { "key": "hostcase", "long": "hostcase", "arg": "none", "scope": "profile", "target": "profile.unsupported", "note": "httpTamper" }, - { "key": "hostspell", "long": "hostspell", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "httpTamper" }, - { "key": "hostnospace", "long": "hostnospace", "arg": "none", "scope": "profile", "target": "profile.unsupported", "note": "httpTamper" }, - { "key": "domcase", "long": "domcase", "arg": "none", "scope": "profile", "target": "profile.unsupported", "note": "httpTamper" }, - { "key": "methodeol", "long": "methodeol", "arg": "none", "scope": "profile", "target": "profile.unsupported", "note": "httpTamper" }, - - { "key": "hostlist", "long": "hostlist", "arg": "required", "scope": "profile", "grammar": "str", "target": "filters.hosts_ref" }, - { "key": "hostlist_domains", "long": "hostlist-domains", "arg": "required", "scope": "profile", "grammar": "csv", "target": "filters.hosts_list" }, - { "key": "hostlist_exclude", "long": "hostlist-exclude", "arg": "required", "scope": "profile", "grammar": "str", "target": "filters.hosts_exclude_ref" }, - { "key": "hostlist_excl_dom", "long": "hostlist-exclude-domains", "arg": "required", "scope": "profile", "grammar": "csv", "target": "filters.hosts_exclude" }, - { "key": "hostlist_auto", "long": "hostlist-auto", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "hostlistAuto" }, - { "key": "hl_auto_fail_thr", "long": "hostlist-auto-fail-threshold", "arg": "required", "scope": "profile", "grammar": "int", "target": "_.na", "note": "hostlistAuto" }, - { "key": "hl_auto_fail_time", "long": "hostlist-auto-fail-time", "arg": "required", "scope": "profile", "grammar": "int", "target": "_.na", "note": "hostlistAuto" }, - { "key": "hl_auto_retrans", "long": "hostlist-auto-retrans-threshold", "arg": "required", "scope": "profile", "grammar": "int", "target": "_.na", "note": "hostlistAuto" }, - { "key": "hl_auto_debug", "long": "hostlist-auto-debug", "arg": "required", "scope": "profile", "grammar": "str", "target": "_.na", "note": "hostlistAuto" }, - - { "key": "ipset", "long": "ipset", "arg": "required", "scope": "profile", "grammar": "str", "target": "filters.ips_ref" }, - { "key": "ipset_ip", "long": "ipset-ip", "arg": "required", "scope": "profile", "grammar": "csv", "target": "filters.ips_list" }, - { "key": "ipset_exclude", "long": "ipset-exclude", "arg": "required", "scope": "profile", "grammar": "str", "target": "profile.unsupported", "note": "excludeListUnsupported" }, - { "key": "ipset_excl_ip", "long": "ipset-exclude-ip", "arg": "required", "scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "excludeListUnsupported" }, - - { "key": "filter_l3", "long": "filter-l3", "arg": "required", "scope": "profile", "grammar": "csv", "target": "filters.l3" }, - { "key": "filter_tcp", "long": "filter-tcp", "arg": "required", "scope": "profile", "grammar": "zapret.ports", "target": "filters.tcp_ports" }, - { "key": "filter_udp", "long": "filter-udp", "arg": "required", "scope": "profile", "grammar": "zapret.ports", "target": "filters.udp_ports" }, - { "key": "filter_l7", "long": "filter-l7", "arg": "required", "scope": "profile", "grammar": "csv", "target": "filters.l7" }, - { "key": "filter_ssid", "long": "filter-ssid", "arg": "required", "scope": "profile", "grammar": "csv", "target": "profile.unsupported", "note": "ssidFilter" }, - - { "key": "qnum", "long": "qnum", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "engineDetail" }, - { "key": "daemon", "long": "daemon", "arg": "none", "scope": "global", "target": "_.na", "note": "processControl" }, - { "key": "pidfile", "long": "pidfile", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "processControl" }, - { "key": "user", "long": "user", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "processControl" }, - { "key": "uid", "long": "uid", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "processControl" }, - { "key": "debug", "long": "debug", "arg": "optional", "scope": "global", "grammar": "str", "target": "_.na", "note": "processControl" }, - { "key": "dry_run", "long": "dry-run", "arg": "none", "scope": "global", "target": "_.ignore" }, - { "key": "version", "long": "version", "arg": "none", "scope": "global", "target": "_.ignore" }, - { "key": "comment", "long": "comment", "arg": "optional", "scope": "global", "grammar": "str", "target": "_.ignore" }, - { "key": "bind_fix4", "long": "bind-fix4", "arg": "none", "scope": "global", "target": "_.na", "note": "engineDetail" }, - { "key": "bind_fix6", "long": "bind-fix6", "arg": "none", "scope": "global", "target": "_.na", "note": "engineDetail" }, - { "key": "ctrack_to", "long": "ctrack-timeouts", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "engineDetail" }, - { "key": "ctrack_off", "long": "ctrack-disable", "arg": "optional", "scope": "global", "grammar": "bool01", "target": "_.na", "note": "engineDetail" }, - { "key": "ipcache_lt", "long": "ipcache-lifetime", "arg": "required", "scope": "global", "grammar": "int", "target": "_.na", "note": "engineDetail" }, - { "key": "ipcache_hn", "long": "ipcache-hostname", "arg": "optional", "scope": "global", "grammar": "bool01", "target": "_.na", "note": "engineDetail" }, - - { "key": "wf_iface", "long": "wf-iface", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "windowsOnly" }, - { "key": "wf_l3", "long": "wf-l3", "arg": "required", "scope": "global", "grammar": "csv", "target": "_.na", "note": "windowsOnly" }, - { "key": "wf_tcp", "long": "wf-tcp", "arg": "required", "scope": "global", "grammar": "zapret.ports", "target": "_.na", "note": "windowsOnly" }, - { "key": "wf_udp", "long": "wf-udp", "arg": "required", "scope": "global", "grammar": "zapret.ports", "target": "_.na", "note": "windowsOnly" }, - { "key": "wf_raw", "long": "wf-raw", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "windowsOnly" }, - { "key": "wf_raw_part", "long": "wf-raw-part", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "windowsOnly" }, - { "key": "wf_filter_lan","long": "wf-filter-lan","arg": "optional", "scope": "global", "grammar": "bool01", "target": "_.na", "note": "windowsOnly" }, - { "key": "wf_save", "long": "wf-save", "arg": "required", "scope": "global", "grammar": "str", "target": "_.na", "note": "windowsOnly" }, - { "key": "ssid_filter", "long": "ssid-filter", "arg": "required", "scope": "global", "grammar": "csv", "target": "_.na", "note": "windowsOnly" }, - { "key": "nlm_filter", "long": "nlm-filter", "arg": "required", "scope": "global", "grammar": "csv", "target": "_.na", "note": "windowsOnly" }, - { "key": "nlm_list", "long": "nlm-list", "arg": "optional", "scope": "global", "grammar": "str", "target": "_.na", "note": "windowsOnly" } + { + "key": "new", + "long": "new", + "arg": "none", + "scope": "break", + "target": "trigger" + }, + { + "key": "skip", + "long": "skip", + "arg": "none", + "scope": "profile", + "target": "profile.skip" + }, + { + "key": "desync", + "long": "dpi-desync", + "arg": "required", + "scope": "profile", + "grammar": "zapret.desync", + "target": "desync.modes" + }, + { + "key": "split_pos", + "long": "dpi-desync-split-pos", + "arg": "required", + "scope": "profile", + "grammar": "zapret.splitpos", + "target": "splits.positions" + }, + { + "key": "fooling", + "long": "dpi-desync-fooling", + "arg": "required", + "scope": "profile", + "grammar": "zapret.fooling", + "target": "fake.fooling" + }, + { + "key": "desync_ttl", + "long": "dpi-desync-ttl", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "fake.ttl" + }, + { + "key": "repeats", + "long": "dpi-desync-repeats", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "fake.repeats" + }, + { + "key": "badseq_inc", + "long": "dpi-desync-badseq-increment", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "fake.seq_increment" + }, + { + "key": "ts_inc", + "long": "dpi-desync-ts-increment", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "fake.ts_increment" + }, + { + "key": "seqovl", + "long": "dpi-desync-split-seqovl", + "arg": "required", + "scope": "profile", + "grammar": "zapret.pos", + "target": "profile.seqovl_len" + }, + { + "key": "seqovl_pat", + "long": "dpi-desync-split-seqovl-pattern", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.seqovl_pattern" + }, + { + "key": "fake_tls", + "long": "dpi-desync-fake-tls", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "fake.blob" + }, + { + "key": "fake_tls_mod", + "long": "dpi-desync-fake-tls-mod", + "arg": "required", + "scope": "profile", + "grammar": "zapret.modlist", + "target": "fake.tls_sni" + }, + { + "key": "fake_quic", + "long": "dpi-desync-fake-quic", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "fake.quic" + }, + { + "key": "fake_http", + "long": "dpi-desync-fake-http", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "httpFakePayload" + }, + { + "key": "fake_unknown", + "long": "dpi-desync-fake-unknown", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fake_syndata", + "long": "dpi-desync-fake-syndata", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fake_wg", + "long": "dpi-desync-fake-wireguard", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fake_dht", + "long": "dpi-desync-fake-dht", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fake_discord", + "long": "dpi-desync-fake-discord", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fake_stun", + "long": "dpi-desync-fake-stun", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fake_unk_udp", + "long": "dpi-desync-fake-unknown-udp", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "autottl", + "long": "dpi-desync-autottl", + "arg": "required", + "scope": "profile", + "grammar": "zapret.autottl", + "target": "profile.unsupported", + "note": "autoTTLUnsupported" + }, + { + "key": "autottl6", + "long": "dpi-desync-autottl6", + "arg": "required", + "scope": "profile", + "grammar": "zapret.autottl", + "target": "profile.unsupported", + "note": "autoTTLUnsupported" + }, + { + "key": "desync_ttl6", + "long": "dpi-desync-ttl6", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "ipv6OnlyKnob" + }, + { + "key": "desync_start", + "long": "dpi-desync-start", + "arg": "required", + "scope": "profile", + "grammar": "zapret.startcutoff", + "target": "profile.unsupported", + "note": "packetWindowUnsupported" + }, + { + "key": "desync_cutoff", + "long": "dpi-desync-cutoff", + "arg": "required", + "scope": "profile", + "grammar": "zapret.startcutoff", + "target": "profile.unsupported", + "note": "packetWindowUnsupported" + }, + { + "key": "flags_set", + "long": "dpi-desync-tcp-flags-set", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "flags_unset", + "long": "dpi-desync-tcp-flags-unset", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fake_tcp_mod", + "long": "dpi-desync-fake-tcp-mod", + "arg": "required", + "scope": "profile", + "grammar": "zapret.modlist", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fakedsplit_mod", + "long": "dpi-desync-fakedsplit-mod", + "arg": "required", + "scope": "profile", + "grammar": "zapret.modlist", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "fakedsplit_pat", + "long": "dpi-desync-fakedsplit-pattern", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "hfs_midhost", + "long": "dpi-desync-hostfakesplit-midhost", + "arg": "required", + "scope": "profile", + "grammar": "zapret.pos", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "hfs_mod", + "long": "dpi-desync-hostfakesplit-mod", + "arg": "required", + "scope": "profile", + "grammar": "zapret.modlist", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "ipfrag_tcp", + "long": "dpi-desync-ipfrag-pos-tcp", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "ipFragPosition" + }, + { + "key": "ipfrag_udp", + "long": "dpi-desync-ipfrag-pos-udp", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "ipFragPosition" + }, + { + "key": "udplen_inc", + "long": "dpi-desync-udplen-increment", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "udplen_pat", + "long": "dpi-desync-udplen-pattern", + "arg": "required", + "scope": "profile", + "grammar": "zapret.blob", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "any_protocol", + "long": "dpi-desync-any-protocol", + "arg": "optional", + "scope": "profile", + "grammar": "bool01", + "target": "profile.unsupported", + "note": "anyProtocol" + }, + { + "key": "skip_nosni", + "long": "dpi-desync-skip-nosni", + "arg": "optional", + "scope": "profile", + "grammar": "bool01", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "desync_fwmark", + "long": "dpi-desync-fwmark", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "sockarg", + "long": "dpi-desync-sockarg", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "dup", + "long": "dup", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.duplicate" + }, + { + "key": "dup_replace", + "long": "dup-replace", + "arg": "optional", + "scope": "profile", + "grammar": "bool01", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "dup_ttl", + "long": "dup-ttl", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "dupKnobUnsupported" + }, + { + "key": "dup_ttl6", + "long": "dup-ttl6", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "dupKnobUnsupported" + }, + { + "key": "dup_autottl", + "long": "dup-autottl", + "arg": "required", + "scope": "profile", + "grammar": "zapret.autottl", + "target": "profile.unsupported", + "note": "autoTTLUnsupported" + }, + { + "key": "dup_autottl6", + "long": "dup-autottl6", + "arg": "required", + "scope": "profile", + "grammar": "zapret.autottl", + "target": "profile.unsupported", + "note": "autoTTLUnsupported" + }, + { + "key": "dup_fooling", + "long": "dup-fooling", + "arg": "required", + "scope": "profile", + "grammar": "zapret.fooling", + "target": "profile.unsupported", + "note": "dupKnobUnsupported" + }, + { + "key": "dup_ts_inc", + "long": "dup-ts-increment", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "dupKnobUnsupported" + }, + { + "key": "dup_badseq", + "long": "dup-badseq-increment", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "dupKnobUnsupported" + }, + { + "key": "dup_badack", + "long": "dup-badack-increment", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "dupKnobUnsupported" + }, + { + "key": "dup_ip_id", + "long": "dup-ip-id", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "dupKnobUnsupported" + }, + { + "key": "dup_start", + "long": "dup-start", + "arg": "required", + "scope": "profile", + "grammar": "zapret.startcutoff", + "target": "profile.unsupported", + "note": "packetWindowUnsupported" + }, + { + "key": "dup_cutoff", + "long": "dup-cutoff", + "arg": "required", + "scope": "profile", + "grammar": "zapret.startcutoff", + "target": "profile.unsupported", + "note": "packetWindowUnsupported" + }, + { + "key": "dup_flags_set", + "long": "dup-tcp-flags-set", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "dup_flags_unset", + "long": "dup-tcp-flags-unset", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "wssize", + "long": "wssize", + "arg": "required", + "scope": "profile", + "grammar": "zapret.wsize", + "target": "profile.win_size" + }, + { + "key": "wsize", + "long": "wsize", + "arg": "required", + "scope": "profile", + "grammar": "zapret.wsize", + "target": "profile.unsupported", + "note": "obsoleteOption" + }, + { + "key": "wssize_cutoff", + "long": "wssize-cutoff", + "arg": "required", + "scope": "profile", + "grammar": "zapret.startcutoff", + "target": "profile.unsupported", + "note": "packetWindowUnsupported" + }, + { + "key": "wssize_forced", + "long": "wssize-forced-cutoff", + "arg": "optional", + "scope": "profile", + "grammar": "bool01", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "orig_ttl", + "long": "orig-ttl", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "origPacketTTL" + }, + { + "key": "orig_ttl6", + "long": "orig-ttl6", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "profile.unsupported", + "note": "origPacketTTL" + }, + { + "key": "orig_autottl", + "long": "orig-autottl", + "arg": "required", + "scope": "profile", + "grammar": "zapret.autottl", + "target": "profile.unsupported", + "note": "origPacketTTL" + }, + { + "key": "orig_autottl6", + "long": "orig-autottl6", + "arg": "required", + "scope": "profile", + "grammar": "zapret.autottl", + "target": "profile.unsupported", + "note": "origPacketTTL" + }, + { + "key": "orig_flags_set", + "long": "orig-tcp-flags-set", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "orig_flags_unset", + "long": "orig-tcp-flags-unset", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "orig_mod_start", + "long": "orig-mod-start", + "arg": "required", + "scope": "profile", + "grammar": "zapret.startcutoff", + "target": "profile.unsupported", + "note": "packetWindowUnsupported" + }, + { + "key": "orig_mod_cutoff", + "long": "orig-mod-cutoff", + "arg": "required", + "scope": "profile", + "grammar": "zapret.startcutoff", + "target": "profile.unsupported", + "note": "packetWindowUnsupported" + }, + { + "key": "synack_split", + "long": "synack-split", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "ip_id", + "long": "ip-id", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "noEquivalent" + }, + { + "key": "hostcase", + "long": "hostcase", + "arg": "none", + "scope": "profile", + "target": "profile.unsupported", + "note": "httpTamper" + }, + { + "key": "hostspell", + "long": "hostspell", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "httpTamper" + }, + { + "key": "hostnospace", + "long": "hostnospace", + "arg": "none", + "scope": "profile", + "target": "profile.unsupported", + "note": "httpTamper" + }, + { + "key": "domcase", + "long": "domcase", + "arg": "none", + "scope": "profile", + "target": "profile.unsupported", + "note": "httpTamper" + }, + { + "key": "methodeol", + "long": "methodeol", + "arg": "none", + "scope": "profile", + "target": "profile.unsupported", + "note": "httpTamper" + }, + { + "key": "hostlist", + "long": "hostlist", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "filters.hosts_ref" + }, + { + "key": "hostlist_domains", + "long": "hostlist-domains", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "filters.hosts_list" + }, + { + "key": "hostlist_exclude", + "long": "hostlist-exclude", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "filters.hosts_exclude_ref" + }, + { + "key": "hostlist_excl_dom", + "long": "hostlist-exclude-domains", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "filters.hosts_exclude" + }, + { + "key": "hostlist_auto", + "long": "hostlist-auto", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "hostlistAuto" + }, + { + "key": "hl_auto_fail_thr", + "long": "hostlist-auto-fail-threshold", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "_.na", + "note": "hostlistAuto" + }, + { + "key": "hl_auto_fail_time", + "long": "hostlist-auto-fail-time", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "_.na", + "note": "hostlistAuto" + }, + { + "key": "hl_auto_retrans", + "long": "hostlist-auto-retrans-threshold", + "arg": "required", + "scope": "profile", + "grammar": "int", + "target": "_.na", + "note": "hostlistAuto" + }, + { + "key": "hl_auto_debug", + "long": "hostlist-auto-debug", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "_.na", + "note": "hostlistAuto" + }, + { + "key": "ipset", + "long": "ipset", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "filters.ips_ref" + }, + { + "key": "ipset_ip", + "long": "ipset-ip", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "filters.ips_list" + }, + { + "key": "ipset_exclude", + "long": "ipset-exclude", + "arg": "required", + "scope": "profile", + "grammar": "str", + "target": "profile.unsupported", + "note": "excludeListUnsupported" + }, + { + "key": "ipset_excl_ip", + "long": "ipset-exclude-ip", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "excludeListUnsupported" + }, + { + "key": "filter_l3", + "long": "filter-l3", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "filters.l3" + }, + { + "key": "filter_tcp", + "long": "filter-tcp", + "arg": "required", + "scope": "profile", + "grammar": "zapret.ports", + "target": "filters.tcp_ports" + }, + { + "key": "filter_udp", + "long": "filter-udp", + "arg": "required", + "scope": "profile", + "grammar": "zapret.ports", + "target": "filters.udp_ports" + }, + { + "key": "filter_l7", + "long": "filter-l7", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "filters.l7" + }, + { + "key": "filter_ssid", + "long": "filter-ssid", + "arg": "required", + "scope": "profile", + "grammar": "csv", + "target": "profile.unsupported", + "note": "ssidFilter" + }, + { + "key": "qnum", + "long": "qnum", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "daemon", + "long": "daemon", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "processControl" + }, + { + "key": "pidfile", + "long": "pidfile", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "processControl" + }, + { + "key": "user", + "long": "user", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "processControl" + }, + { + "key": "uid", + "long": "uid", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "processControl" + }, + { + "key": "debug", + "long": "debug", + "arg": "optional", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "processControl" + }, + { + "key": "dry_run", + "long": "dry-run", + "arg": "none", + "scope": "global", + "target": "_.ignore" + }, + { + "key": "version", + "long": "version", + "arg": "none", + "scope": "global", + "target": "_.ignore" + }, + { + "key": "comment", + "long": "comment", + "arg": "optional", + "scope": "global", + "grammar": "str", + "target": "_.ignore" + }, + { + "key": "bind_fix4", + "long": "bind-fix4", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "bind_fix6", + "long": "bind-fix6", + "arg": "none", + "scope": "global", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "ctrack_to", + "long": "ctrack-timeouts", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "ctrack_off", + "long": "ctrack-disable", + "arg": "optional", + "scope": "global", + "grammar": "bool01", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "ipcache_lt", + "long": "ipcache-lifetime", + "arg": "required", + "scope": "global", + "grammar": "int", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "ipcache_hn", + "long": "ipcache-hostname", + "arg": "optional", + "scope": "global", + "grammar": "bool01", + "target": "_.na", + "note": "engineDetail" + }, + { + "key": "wf_iface", + "long": "wf-iface", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "wf_l3", + "long": "wf-l3", + "arg": "required", + "scope": "global", + "grammar": "csv", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "wf_tcp", + "long": "wf-tcp", + "arg": "required", + "scope": "global", + "grammar": "zapret.ports", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "wf_udp", + "long": "wf-udp", + "arg": "required", + "scope": "global", + "grammar": "zapret.ports", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "wf_raw", + "long": "wf-raw", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "wf_raw_part", + "long": "wf-raw-part", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "wf_filter_lan", + "long": "wf-filter-lan", + "arg": "optional", + "scope": "global", + "grammar": "bool01", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "wf_save", + "long": "wf-save", + "arg": "required", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "ssid_filter", + "long": "ssid-filter", + "arg": "required", + "scope": "global", + "grammar": "csv", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "nlm_filter", + "long": "nlm-filter", + "arg": "required", + "scope": "global", + "grammar": "csv", + "target": "_.na", + "note": "windowsOnly" + }, + { + "key": "nlm_list", + "long": "nlm-list", + "arg": "optional", + "scope": "global", + "grammar": "str", + "target": "_.na", + "note": "windowsOnly" + } ] } diff --git a/src/convert/spec.go b/src/convert/spec.go index b6fe649b..08524ea2 100644 --- a/src/convert/spec.go +++ b/src/convert/spec.go @@ -71,11 +71,18 @@ type DetectSpec struct { EnvVars []string `json:"env_vars"` } +type SpecDefaults struct { + FakeTTL int `json:"fake_ttl"` + FakeTTLForced bool `json:"fake_ttl_forced"` + OOBByte int `json:"oob_byte"` +} + type Spec struct { Tool string `json:"tool"` Label string `json:"label"` Style string `json:"style"` Homepage string `json:"homepage"` + Defaults SpecDefaults `json:"defaults"` Detect DetectSpec `json:"detect"` Versions []VersionSpec `json:"versions"` Ambiguous []string `json:"ambiguous"` diff --git a/src/convert/tool.go b/src/convert/tool.go new file mode 100644 index 00000000..f7569a3e --- /dev/null +++ b/src/convert/tool.go @@ -0,0 +1,28 @@ +package convert + +import "github.com/daniellavrushin/b4/config" + +type normalizer func(*Program, []Token, *noteSet) + +type toolEmitter func(*config.SetConfig, *Profile, tokenIndex, *noteSet) + +var ( + normalizers = map[string]normalizer{} + toolEmitters = map[string]toolEmitter{} +) + +func registerNormalizer(name string, fn normalizer) { normalizers[name] = fn } + +func registerToolEmitter(tool string, fn toolEmitter) { toolEmitters[tool] = fn } + +func runNormalizer(name string, prog *Program, tokens []Token, notes *noteSet) { + if fn, ok := normalizers[name]; ok { + fn(prog, tokens, notes) + } +} + +func runToolEmitter(tool string, set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { + if fn, ok := toolEmitters[tool]; ok { + fn(set, prof, ti, notes) + } +} diff --git a/src/convert/tool_byedpi.go b/src/convert/tool_byedpi.go new file mode 100644 index 00000000..3ee07eec --- /dev/null +++ b/src/convert/tool_byedpi.go @@ -0,0 +1,149 @@ +package convert + +import ( + "errors" + "strings" +) + +const byedpiV013 = "0.13" + +func init() { + grammars["byedpi.pos"] = gByedpiPos + grammars["cchar"] = gCChar + grammars["cdata"] = gCData + grammars["hostlist"] = gHostList + grammars["iplist"] = gHostList +} + +func gCChar(raw string, _ grammarCtx) (Value, error) { + dec, err := parseCForm(raw) + if err != nil { + return Value{}, err + } + if len(dec) != 1 { + return Value{}, errors.New("expected exactly one byte") + } + return Value{Byte: dec[0], Str: raw}, nil +} + +func gCData(raw string, _ grammarCtx) (Value, error) { + if strings.HasPrefix(raw, ":") { + dec, err := parseCForm(raw[1:]) + if err != nil { + return Value{}, err + } + return Value{Str: string(dec)}, nil + } + return Value{Ref: raw}, nil +} + +func gHostList(raw string, _ grammarCtx) (Value, error) { + if !strings.HasPrefix(raw, ":") { + return Value{Ref: raw}, nil + } + fields := strings.FieldsFunc(raw[1:], func(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == ',' || r == ';' + }) + out := make([]string, 0, len(fields)) + for _, f := range fields { + if f != "" { + out = append(out, f) + } + } + if len(out) == 0 { + return Value{}, errors.New("expected at least one entry") + } + return Value{List: out, Str: raw}, nil +} + +func gByedpiPos(raw string, ctx grammarCtx) (Value, error) { + if ctx.Version == byedpiV013 { + p, err := parsePosV013(raw) + return Value{Pos: p, Str: raw}, err + } + p, err := parsePosV017(raw) + return Value{Pos: p, Str: raw}, err +} + +func parsePosV013(raw string) (Pos, error) { + p := Pos{Raw: raw, Anchor: AnchorAbs, Rel: RelStart} + n, rest, err := splitLeadingInt(raw) + if err != nil { + return p, err + } + p.Offset = n + if rest == "" { + return p, nil + } + if rest[0] != '+' || len(rest) != 2 { + return p, errors.New("expected , +s, +h or +e") + } + switch rest[1] { + case 's': + p.Anchor, p.Rel = AnchorSNI, RelStart + case 'h': + p.Anchor, p.Rel = AnchorHost, RelStart + case 'e': + p.Anchor, p.Rel = AnchorPacket, RelEnd + default: + return p, errors.New("expected +s, +h or +e") + } + return p, nil +} + +func parsePosV017(raw string) (Pos, error) { + p := Pos{Raw: raw, Anchor: AnchorAbs, Rel: RelStart} + n, rest, err := splitLeadingInt(raw) + if err != nil { + return p, err + } + p.Offset = n + for len(rest) > 0 && rest[0] == ':' { + var v int + v, rest, err = splitLeadingInt(rest[1:]) + if err != nil || v < 0 { + return p, errors.New("expected [:repeats[:skip]]") + } + if p.Repeats == 0 { + if v == 0 { + return p, errors.New("repeats must be greater than zero") + } + p.Repeats = v + } else { + p.Skip = v + break + } + } + if rest == "" { + return p, nil + } + if rest[0] != '+' || len(rest) < 2 { + return p, errors.New("expected +s, +h or +n after the offset") + } + switch rest[1] { + case 's': + p.Anchor = AnchorSNI + case 'h': + p.Anchor = AnchorHost + case 'n': + p.Anchor = AnchorPacket + default: + return p, errors.New("expected +s, +h or +n after the offset") + } + if len(rest) > 2 { + switch rest[2] { + case 'e': + p.Rel = RelEnd + case 'm': + p.Rel = RelMid + case 'r': + p.Rel = RelRand + case 's': + p.Rel = RelStart + } + } + if p.Anchor == AnchorPacket && p.Rel == RelStart { + p.Anchor = AnchorAbs + } + return p, nil +} diff --git a/src/convert/tool_byedpi_test.go b/src/convert/tool_byedpi_test.go new file mode 100644 index 00000000..7ed2a92c --- /dev/null +++ b/src/convert/tool_byedpi_test.go @@ -0,0 +1,738 @@ +package convert + +import ( + "testing" + + "github.com/daniellavrushin/b4/config" +) + +func TestAnalyze_Splitting(t *testing.T) { + tests := []struct { + name string + line string + strategy string + middleSNI bool + sniPosition int + }{ + {"sniStart", "-s1+s", "tcp", true, 0}, + {"sniMiddle", "-s0+sm", "tcp", true, 0}, + {"fixedPosition", "-s5", "tcp", false, 5}, + {"firstByte", "-s1", "tcp", false, 1}, + {"disorder", "-d0+sm", "disorder", true, 0}, + {"splitAndDisorder", "-s1 -d0+sm", "combo", true, 0}, + {"noSplit", "-t8", "none", true, 1}, + {"negativeOffset", "-s-1", "tcp", true, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := analyze(t, tt.line) + set := res.Sets[0] + if set.Fragmentation.Strategy != tt.strategy { + t.Fatalf("strategy: got %q, want %q", set.Fragmentation.Strategy, tt.strategy) + } + if tt.strategy == config.ConfigNone { + return + } + if set.Fragmentation.MiddleSNI != tt.middleSNI { + t.Fatalf("middle_sni: got %v, want %v", set.Fragmentation.MiddleSNI, tt.middleSNI) + } + if set.Fragmentation.SNIPosition != tt.sniPosition { + t.Fatalf("sni_position: got %d, want %d", set.Fragmentation.SNIPosition, tt.sniPosition) + } + }) + } +} + +func TestAnalyze_OOBUsesByedpiDefaultByte(t *testing.T) { + res := analyze(t, "-o1") + set := res.Sets[0] + if set.Fragmentation.Strategy != "oob" { + t.Fatalf("strategy: got %q", set.Fragmentation.Strategy) + } + if set.Fragmentation.OOBPosition != 1 { + t.Fatalf("oob_position: got %d", set.Fragmentation.OOBPosition) + } + if set.Fragmentation.OOBChar != 'a' { + t.Fatalf("oob_char: got %d, want %d (byedpi default), b4 default is %d", + set.Fragmentation.OOBChar, 'a', config.DefaultSetConfig.Fragmentation.OOBChar) + } +} + +func TestAnalyze_OOBByteOverride(t *testing.T) { + res := analyze(t, "-o1 -eb") + if got := res.Sets[0].Fragmentation.OOBChar; got != 'b' { + t.Fatalf("oob_char: got %d, want %d", got, 'b') + } +} + +func TestAnalyze_TLSRecord(t *testing.T) { + res := analyze(t, "-r2") + set := res.Sets[0] + if set.Fragmentation.Strategy != "tls" || set.Fragmentation.TLSRecordPosition != 2 { + t.Fatalf("got strategy=%q pos=%d", set.Fragmentation.Strategy, set.Fragmentation.TLSRecordPosition) + } +} + +func TestAnalyze_FakeDefaults(t *testing.T) { + res := analyze(t, "-f-1") + set := res.Sets[0] + if !set.Faking.SNI { + t.Fatal("expected faking.sni to be enabled") + } + if set.Faking.Strategy != "ttl" || !set.Faking.ApplyTTL { + t.Fatalf("got strategy=%q apply_ttl=%v", set.Faking.Strategy, set.Faking.ApplyTTL) + } + if set.Faking.TTL != byedpiFakeTTL(t) { + t.Fatalf("ttl: got %d, want %d", set.Faking.TTL, byedpiFakeTTL(t)) + } +} + +func TestAnalyze_FakeSNIBecomesGeneratedPayload(t *testing.T) { + res := analyze(t, "-f-1 -Qr -n https://www.gosuslugi.ru/") + set := res.Sets[0] + if set.Faking.SNIType != config.FakePayloadDomain { + t.Fatalf("sni_type: got %d, want %d", set.Faking.SNIType, config.FakePayloadDomain) + } + if set.Faking.PayloadDomain != "www.gosuslugi.ru" { + t.Fatalf("payload_domain: got %q", set.Faking.PayloadDomain) + } + if len(set.Faking.TLSMod) != 1 || set.Faking.TLSMod[0] != "rnd" { + t.Fatalf("tls_mod: got %v", set.Faking.TLSMod) + } +} + +func TestAnalyze_MD5SigWithoutFakeIsDegenerate(t *testing.T) { + res := analyze(t, "-d0+sm -S") + n := noteFor(t, res, "-S") + if n.Status != StatusDegenerate || n.Reason != "requiresFake" { + t.Fatalf("got %+v", n) + } +} + +func TestAnalyze_RepeatsWithoutSkipIsDegenerate(t *testing.T) { + res := analyze(t, "-d1:11+sm") + n := noteFor(t, res, "-d1:11+sm") + if n.Status != StatusDegenerate || n.Reason != "repeatsWithoutSkip" { + t.Fatalf("got %+v", n) + } +} + +func TestAnalyze_RepeatsWithSkipIsApproximated(t *testing.T) { + res := analyze(t, "-s1:3:5") + n := noteFor(t, res, "-s1:3:5") + if n.Status != StatusApproximated || n.Reason != "repeatsUnsupported" { + t.Fatalf("got %+v", n) + } +} + +func TestAnalyze_HostsInlineBecomeTargets(t *testing.T) { + res := analyze(t, "-H:youtube.com,googlevideo.com -s1+s") + set := res.Sets[0] + if len(set.Targets.SNIDomains) != 2 { + t.Fatalf("sni_domains: got %v", set.Targets.SNIDomains) + } + if !set.Enabled { + t.Fatal("a set with targets should be enabled") + } +} + +func TestAnalyze_HostsFileIsUnresolved(t *testing.T) { + res := analyze(t, "-H /etc/byedpi/hosts.txt -s1+s") + if len(res.Unresolved) != 1 || res.Unresolved[0].Kind != "hostlist" { + t.Fatalf("unresolved: got %+v", res.Unresolved) + } + if res.Sets[0].Enabled { + t.Fatal("a set with no resolved targets must stay disabled") + } +} + +func TestAnalyze_ProxyRuntimeIsNotApplicable(t *testing.T) { + res := analyze(t, "-i 0.0.0.0 -p 1080 -c 512 -s1+s") + for _, tok := range []string{"-i 0.0.0.0", "-p 1080", "-c 512"} { + n := noteFor(t, res, tok) + if n.Status != StatusNotApplicable { + t.Fatalf("%s: got %+v", tok, n) + } + } + if res.Fidelity.NotApplicable != 3 { + t.Fatalf("not_applicable: got %d", res.Fidelity.NotApplicable) + } + bare := analyze(t, "-s1+s") + if res.Fidelity.Score != bare.Fidelity.Score { + t.Fatalf("proxy plumbing must not change the score: got %d with, %d without", + res.Fidelity.Score, bare.Fidelity.Score) + } +} + +func TestAnalyze_UnsupportedOptions(t *testing.T) { + tests := []struct { + token string + line string + reason string + }{ + {"-Mh,d,r", "-f1 -Mh,d,r", "httpTamper"}, + {"-O5", "-f1 -O5", "fakeOffsetUnsupported"}, + {"-m3", "-f1 -m3", "noEquivalent"}, + } + for _, tt := range tests { + t.Run(tt.token, func(t *testing.T) { + res := analyze(t, tt.line) + n := noteFor(t, res, tt.token) + if n.Status != StatusUnsupported || n.Reason != tt.reason { + t.Fatalf("got %+v", n) + } + }) + } +} + +func TestAnalyze_UDPProfile(t *testing.T) { + res := analyze(t, "-Ku -a1") + set := res.Sets[0] + if set.UDP.Mode != "fake" || set.UDP.FakeSeqLength != 1 || set.UDP.FilterQUIC != "all" { + t.Fatalf("udp: got mode=%q len=%d quic=%q", set.UDP.Mode, set.UDP.FakeSeqLength, set.UDP.FilterQUIC) + } + if set.Fragmentation.Strategy != config.ConfigNone || set.Faking.SNI { + t.Fatalf("a UDP-only profile must not carry TCP strategies: %q / %v", + set.Fragmentation.Strategy, set.Faking.SNI) + } +} + +func TestAnalyze_ProtoFilterBecomesPortFilter(t *testing.T) { + res := analyze(t, "-Kt,h -H:example.com -s1+s") + if got := res.Sets[0].TCP.DPortFilter; got != "80,443" { + t.Fatalf("dport_filter: got %q", got) + } +} + +func TestAnalyze_PortFilter(t *testing.T) { + res := analyze(t, "-V443-444 -H:example.com -s1+s") + if got := res.Sets[0].TCP.DPortFilter; got != "443-444" { + t.Fatalf("dport_filter: got %q", got) + } +} + +func TestAnalyze_EscalationChain(t *testing.T) { + res := analyze(t, "-H:example.com -s1+s -At -d0+sm -At -f-1") + if len(res.Sets) != 3 { + t.Fatalf("expected 3 sets, got %d", len(res.Sets)) + } + if res.Sets[0].Escalate.To != res.Sets[1].Id { + t.Fatalf("set 0 should escalate to set 1, got %q", res.Sets[0].Escalate.To) + } + if res.Sets[1].Escalate.To != res.Sets[2].Id { + t.Fatalf("set 1 should escalate to set 2, got %q", res.Sets[1].Escalate.To) + } + if res.Sets[2].Escalate.To != "" { + t.Fatalf("last set must not escalate, got %q", res.Sets[2].Escalate.To) + } + for i := 1; i < 3; i++ { + if !res.Sets[i].Enabled { + t.Fatalf("escalation target %d must be enabled", i) + } + if res.Sets[i].TCP.DPortFilter != "" { + t.Fatalf("escalation target %d must not match on ports alone", i) + } + } +} + +func TestAnalyze_AutoNoneIsNotAnEscalation(t *testing.T) { + res := analyze(t, "-Ku -a1 -An -s1+s") + if res.Sets[0].Escalate.To != "" { + t.Fatalf("-An must not create an escalation link, got %q", res.Sets[0].Escalate.To) + } + n := noteFor(t, res, "-An") + if n.Status != StatusMapped || n.Reason != "autoNoneEntrySet" { + t.Fatalf("got %+v", n) + } +} + +func TestAnalyze_UDPOnlyProfileIsFoldedIntoTheEntrySet(t *testing.T) { + res := analyze(t, "-Ku -a1 -An -s1+s -At -d0+sm") + + if len(res.Sets) != 2 { + t.Fatalf("the UDP profile should not become a set of its own, got %d sets", len(res.Sets)) + } + entry := res.Sets[0] + if entry.Fragmentation.Strategy != "tcp" { + t.Fatalf("entry set lost its TCP strategy: %q", entry.Fragmentation.Strategy) + } + if entry.UDP.Mode != "fake" || entry.UDP.FakeSeqLength != 1 || entry.UDP.FilterQUIC != "all" { + t.Fatalf("entry set did not inherit the UDP handling: %+v", entry.UDP) + } + n := noteFor(t, res, "-Ku") + if n.Reason != "udpFoldedIntoSet" { + t.Fatalf("got %+v", n) + } +} + +func TestAnalyze_NoEntrySetIsShadowedByAnother(t *testing.T) { + res, err := Analyze("-Ku -a1 -An -s1+s -At -d0+sm", Options{Domains: []string{"youtube.com"}}) + if err != nil { + t.Fatal(err) + } + claimed := map[string]int{} + for _, s := range res.Sets { + if !s.Enabled { + continue + } + for _, d := range s.Targets.SNIDomains { + claimed[d]++ + } + } + for domain, n := range claimed { + if n > 1 { + t.Fatalf("%q is claimed by %d enabled sets; b4 applies only the first and ignores the rest", domain, n) + } + } +} + +func TestAnalyze_UDPOnlyProfileSurvivesWithoutACarrier(t *testing.T) { + res := analyze(t, "-Ku -a1") + if len(res.Sets) != 1 { + t.Fatalf("expected the UDP profile to stay as its own set, got %d", len(res.Sets)) + } + if res.Sets[0].UDP.FakeSeqLength != 1 { + t.Fatalf("udp: got %+v", res.Sets[0].UDP) + } +} + +func TestAnalyze_UDPProfileWithOwnHostsIsNotFolded(t *testing.T) { + res := analyze(t, "-Ku -H:quic.example.com -a1 -An -H:www.example.com -s1+s") + if len(res.Sets) != 2 { + t.Fatalf("a UDP profile with its own host list is a separate set, got %d", len(res.Sets)) + } +} + +func TestAnalyze_ExplicitVersionOverride(t *testing.T) { + res, err := Analyze("-n example.com -f1", Options{Tool: "byedpi", Version: "0.13"}) + if err != nil { + t.Fatal(err) + } + if res.Version != "0.13" { + t.Fatalf("version: got %q", res.Version) + } + if res.Sets[0].Faking.PayloadDomain != "example.com" { + t.Fatalf("payload_domain: got %q", res.Sets[0].Faking.PayloadDomain) + } +} + +func TestAnalyze_UserReportedLine(t *testing.T) { + line := "-Ku -a1 -An -o1 -At,r,s -f-1 -At,r,s -d1:11+sm -S -At,r,s " + + "-n https://www.gosuslugi.ru/ -Qr -f1 -d1:11+sm -s1:11+sm -S" + res := analyze(t, line) + + if res.Tool != "byedpi" { + t.Fatalf("tool: got %q", res.Tool) + } + if res.Version != "0.17" { + t.Fatalf("version: got %q, want 0.17 (uses -Q and pos:repeats syntax)", res.Version) + } + if res.VersionInferred { + t.Fatal("version should be detected from markers, not guessed") + } + if len(res.Sets) != 4 { + t.Fatalf("expected 4 sets, got %d", len(res.Sets)) + } + + if res.Sets[0].UDP.FakeSeqLength != 1 { + t.Fatalf("set 0 should carry the folded UDP handling: got %d", res.Sets[0].UDP.FakeSeqLength) + } + if res.Sets[0].Fragmentation.Strategy != "oob" || res.Sets[0].Fragmentation.OOBPosition != 1 { + t.Fatalf("set 0: got %+v", res.Sets[0].Fragmentation) + } + if res.Sets[0].Escalate.To != res.Sets[1].Id || + res.Sets[1].Escalate.To != res.Sets[2].Id || + res.Sets[2].Escalate.To != res.Sets[3].Id { + t.Fatal("expected sets 0 -> 1 -> 2 -> 3 escalation chain") + } + if res.Sets[3].Faking.PayloadDomain != "www.gosuslugi.ru" || !res.Sets[3].Faking.MD5OnFake { + t.Fatalf("set 3 faking: got %+v", res.Sets[3].Faking) + } + if res.Sets[3].Fragmentation.Strategy != "combo" { + t.Fatalf("set 3 strategy: got %q", res.Sets[3].Fragmentation.Strategy) + } + if !res.Sets[3].Fragmentation.Combo.DecoyEnabled { + t.Fatal("a combo profile that also carries -f should enable the decoy") + } + + n := noteFor(t, res, "-n https://www.gosuslugi.ru/") + if n.Status != StatusApproximated || n.Reason != "fakeSNINormalised" { + t.Fatalf("URL passed to --fake-sni should be flagged, got %+v", n) + } + if !hasField(n, "faking.payload_domain") { + t.Fatalf("note should name the field it set, got %+v", n) + } + + var needsTargets bool + for _, w := range res.Warnings { + if w.Code == "needsTargets" { + needsTargets = true + } + } + if !needsTargets { + t.Fatal("a byedpi line with no host filter must warn that targets are required") + } +} + +func TestParsePosV013_Valid(t *testing.T) { + tests := []struct { + name string + in string + offset int + anchor Anchor + rel Rel + }{ + {"plain", "1", 1, AnchorAbs, RelStart}, + {"negative", "-1", -1, AnchorAbs, RelStart}, + {"hex", "0x10", 16, AnchorAbs, RelStart}, + {"sni", "2+s", 2, AnchorSNI, RelStart}, + {"host", "3+h", 3, AnchorHost, RelStart}, + {"end", "4+e", 4, AnchorPacket, RelEnd}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := parsePosV013(tt.in) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p.Offset != tt.offset || p.Anchor != tt.anchor || p.Rel != tt.rel { + t.Fatalf("got offset=%d anchor=%s rel=%s, want %d/%s/%s", p.Offset, p.Anchor, p.Rel, tt.offset, tt.anchor, tt.rel) + } + }) + } +} + +func TestParsePosV013_Rejects(t *testing.T) { + for _, in := range []string{"1:11+sm", "1+sm", "1+x", "abc", "1+", "1junk"} { + t.Run(in, func(t *testing.T) { + if _, err := parsePosV013(in); err == nil { + t.Fatalf("expected %q to be rejected", in) + } + }) + } +} + +func TestParsePosV017_Valid(t *testing.T) { + tests := []struct { + name string + in string + offset int + repeats int + skip int + anchor Anchor + rel Rel + }{ + {"plain", "1", 1, 0, 0, AnchorAbs, RelStart}, + {"negative", "-1", -1, 0, 0, AnchorAbs, RelStart}, + {"sniMid", "1:11+sm", 1, 11, 0, AnchorSNI, RelMid}, + {"repeatsSkip", "1:3:5", 1, 3, 5, AnchorAbs, RelStart}, + {"sniStart", "0+s", 0, 0, 0, AnchorSNI, RelStart}, + {"sniEnd", "0+se", 0, 0, 0, AnchorSNI, RelEnd}, + {"sniRand", "0+sr", 0, 0, 0, AnchorSNI, RelRand}, + {"hostMid", "2+hm", 2, 0, 0, AnchorHost, RelMid}, + {"packetMid", "0+nm", 0, 0, 0, AnchorPacket, RelMid}, + {"nullBase", "5+n", 5, 0, 0, AnchorAbs, RelStart}, + {"unknownSecondCharIgnored", "5+sX", 5, 0, 0, AnchorSNI, RelStart}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := parsePosV017(tt.in) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p.Offset != tt.offset || p.Repeats != tt.repeats || p.Skip != tt.skip || p.Anchor != tt.anchor || p.Rel != tt.rel { + t.Fatalf("got %+v, want offset=%d repeats=%d skip=%d anchor=%s rel=%s", + p, tt.offset, tt.repeats, tt.skip, tt.anchor, tt.rel) + } + }) + } +} + +func TestParsePosV017_Rejects(t *testing.T) { + for _, in := range []string{"1:0", "abc", "1+x", "1+"} { + t.Run(in, func(t *testing.T) { + if _, err := parsePosV017(in); err == nil { + t.Fatalf("expected %q to be rejected", in) + } + }) + } +} + +func TestGHostList_InlineVsFile(t *testing.T) { + inline, err := gHostList(":a.com b.com,c.com", grammarCtx{}) + if err != nil { + t.Fatal(err) + } + if len(inline.List) != 3 { + t.Fatalf("got %v", inline.List) + } + file, err := gHostList("/etc/byedpi/hosts.txt", grammarCtx{}) + if err != nil { + t.Fatal(err) + } + if file.Ref != "/etc/byedpi/hosts.txt" { + t.Fatalf("got ref %q", file.Ref) + } +} + +func TestGetoptLong_VersionScopedOptions(t *testing.T) { + v13 := getoptLong([]string{"-Qr"}, testTable(t, "0.13"), false) + if v13[0].Err != "unknown" { + t.Fatalf("expected -Q to be unknown in 0.13, got %+v", v13[0]) + } + v17 := getoptLong([]string{"-Qr"}, testTable(t, "0.17"), false) + if v17[0].Key != "fake_tls_mod" { + t.Fatalf("expected -Q to resolve in 0.17, got %+v", v17[0]) + } + + n13 := getoptLong([]string{"-n", "example.com"}, testTable(t, "0.13"), false) + if n13[0].Key != "tls_sni" { + t.Fatalf("expected -n to be tls_sni in 0.13, got %q", n13[0].Key) + } + n17 := getoptLong([]string{"-n", "example.com"}, testTable(t, "0.17"), false) + if n17[0].Key != "fake_sni" { + t.Fatalf("expected -n to be fake_sni in 0.17, got %q", n17[0].Key) + } +} + +func TestDetectVersion_Markers(t *testing.T) { + all, err := loadSpecs() + if err != nil { + t.Fatal(err) + } + spec := all["byedpi"] + tests := []struct { + name string + argv []string + want string + detected bool + }{ + {"fakeTLSMod", []string{"-Qr"}, "0.17", true}, + {"ipOpt", []string{"-k"}, "0.13", true}, + {"posRepeats", []string{"-d1:11+sm"}, "0.17", true}, + {"ambiguousFallsBackToDefault", []string{"-s1", "-f-1"}, "0.17", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, detected := detectVersion(spec, tt.argv) + if got != tt.want || detected != tt.detected { + t.Fatalf("got (%s, %v), want (%s, %v)", got, detected, tt.want, tt.detected) + } + }) + } +} + +var sharedConfigs = []struct { + name string + line string +}{ + { + "gosuslugiEscalation", + "-Ku -a1 -An -o1 -At,r,s -f-1 -At,r,s -d1:11+sm -S -At,r,s " + + "-n https://www.gosuslugi.ru/ -Qr -f1 -d1:11+sm -s1:11+sm -S", + }, + { + "vkLadderTwoProfiles", + "-Ku -a3 -An -Kt,h -n vk.com -d1 -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s " + + "-d25+s -s30+s -d35+s -r1+s -S -Mh,d -As -Kt,h -n vk.com -d1 -d3+s -s6+s " + + "-d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -S -Mh,d", + }, + { + "sevenProfileEscalation", + "-Ku -a1 -An -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s " + + "-d35+s -At,r,s -s1 -q1 -At,r,s -s5 -o25000+s -At,r,s -o1 -d1 -r1+s -t10 " + + "-b1500 -s0+s -d3+s -At,r,s -f-1 -r1+s -At,r,s -s1 -o1+s -s-1", + }, + { + "inlineFakePayloadUDP", + `-Ku -l':\x16\x03\x01\x02\x87\x01\x00\x02\x83\x03\x03\x5f\x15\x63\xcb\x06' ` + + `-a1 -An -s1 -q1 -Y -At -f-1 -r1+s -As`, + }, +} + +func TestCorpus_EveryRecognizedOptionIsReported(t *testing.T) { + for _, tc := range sharedConfigs { + t.Run(tc.name, func(t *testing.T) { + res := analyze(t, tc.line) + reported := map[string]bool{} + for _, n := range res.Notes { + reported[n.Token] = true + } + all, err := loadSpecs() + if err != nil { + t.Fatal(err) + } + table := all[res.Tool].tableFor(res.Version) + for _, tok := range getoptLong(res.Argv, table, false) { + if tok.Spec.Target == "_.ignore" { + continue + } + if !reported[tok.Raw] { + t.Fatalf("option %q produced no entry in the report", tok.Raw) + } + } + }) + } +} + +func TestCorpus_NoUnaccountedOptions(t *testing.T) { + for _, tc := range sharedConfigs { + t.Run(tc.name, func(t *testing.T) { + res := analyze(t, tc.line) + for _, n := range res.Notes { + if n.Reason == "unaccountedOption" { + t.Fatalf("%q fell through every emit rule", n.Token) + } + if n.Status == StatusUnknown || n.Status == StatusInvalid { + t.Fatalf("%q was not understood: %s/%s", n.Token, n.Status, n.Reason) + } + } + }) + } +} + +func TestCorpus_EscalationChainsAreAcyclic(t *testing.T) { + for _, tc := range sharedConfigs { + t.Run(tc.name, func(t *testing.T) { + res := analyze(t, tc.line) + byID := map[string]int{} + for i, s := range res.Sets { + byID[s.Id] = i + } + for i, s := range res.Sets { + if s.Escalate.To == "" { + continue + } + target, ok := byID[s.Escalate.To] + if !ok { + t.Fatalf("set %d escalates to an unknown id %q", i, s.Escalate.To) + } + if target <= i { + t.Fatalf("set %d escalates backwards to %d", i, target) + } + if !res.Sets[target].Enabled { + t.Fatalf("set %d escalates to a disabled set %d", i, target) + } + } + }) + } +} + +func TestAnalyze_UDPProfileStillReportsFakeOptions(t *testing.T) { + res := analyze(t, `-Ku -l':abc' -a1`) + n := noteFor(t, res, "-l:abc") + if n.Status != StatusDegenerate || n.Reason != "requiresFake" { + t.Fatalf("a fake payload in a UDP-only profile must be reported, got %+v", n) + } +} + +func TestAnalyze_ComboHonoursFirstByteSplit(t *testing.T) { + res := analyze(t, "-d1 -s3+s") + set := res.Sets[0] + if set.Fragmentation.Strategy != "combo" { + t.Fatalf("strategy: got %q", set.Fragmentation.Strategy) + } + if !set.Fragmentation.Combo.FirstByteSplit { + t.Fatal("offset 1 should enable combo.first_byte_split") + } + n := noteFor(t, res, "-d1") + if n.Status != StatusMapped || n.Reason != "firstByteMapped" { + t.Fatalf("offset 1 is representable in combo, got %+v", n) + } + if !hasField(n, "fragmentation.combo.first_byte_split") { + t.Fatalf("note should name the field it set, got %+v", n) + } +} + +func TestAnalyze_SplitLadderIsSummarised(t *testing.T) { + res := analyze(t, "-s1 -d3+s -s6+s -d9+s -s12+s -d15+s") + var found *Note + for i := range res.Notes { + if res.Notes[i].Reason == "splitPointsCollapsed" { + found = &res.Notes[i] + } + } + if found == nil { + t.Fatal("a ladder of split points should be summarised once for the profile") + } + if found.Params["count"] != 6 { + t.Fatalf("count: got %v, want 6", found.Params["count"]) + } +} + +func TestAnalyze_ProfileWithoutDesyncIsReported(t *testing.T) { + res := analyze(t, "-s1 -At -f-1 -As") + if len(res.Sets) != 3 { + t.Fatalf("expected 3 sets, got %d", len(res.Sets)) + } + last := res.Sets[2] + if last.Fragmentation.Strategy != "none" || last.Faking.SNI { + t.Fatalf("a trailing -A with no options is a pass-through set, got %+v", last.Fragmentation) + } + var found bool + for _, n := range res.Notes { + if n.Reason == "profileWithoutDesync" && n.Profile == 2 { + found = true + } + } + if !found { + t.Fatal("an empty set must be explained rather than left as a mystery") + } +} + +func TestAnalyze_CompetingFixedPositions(t *testing.T) { + res := analyze(t, "-s5 -s7") + if got := res.Sets[0].Fragmentation.SNIPosition; got != 5 { + t.Fatalf("sni_position: got %d, want 5", got) + } + if n := noteFor(t, res, "-s5"); n.Status != StatusMapped { + t.Fatalf("-s5: got %+v", n) + } + n := noteFor(t, res, "-s7") + if n.Status != StatusApproximated || n.Reason != "fixedPositionIgnored" { + t.Fatalf("a second fixed position cannot be kept, got %+v", n) + } +} + +func TestAnalyze_PositionBeyondRangeIsClamped(t *testing.T) { + res := analyze(t, "-s25000") + if got := res.Sets[0].Fragmentation.SNIPosition; got != maxSNIPosition { + t.Fatalf("sni_position: got %d, want %d", got, maxSNIPosition) + } + n := noteFor(t, res, "-s25000") + if n.Status != StatusApproximated || n.Reason != "positionClamped" { + t.Fatalf("got %+v", n) + } +} + +func TestAnalyze_DroppedOOBDoesNotLeaveItsByteBehind(t *testing.T) { + res := analyze(t, "-s5 -o25000+s") + set := res.Sets[0] + if set.Fragmentation.Strategy == "oob" { + t.Fatal("a plain split should win over oob here") + } + if set.Fragmentation.OOBChar != config.DefaultSetConfig.Fragmentation.OOBChar { + t.Fatalf("oob_char should stay at the b4 default when oob was dropped, got %d", + set.Fragmentation.OOBChar) + } +} + +func byedpiFakeTTL(t *testing.T) uint8 { + t.Helper() + all, err := loadSpecs() + if err != nil { + t.Fatal(err) + } + return uint8(all["byedpi"].Defaults.FakeTTL) +} + +func TestByedpi_DefaultsComeFromTheRuleFile(t *testing.T) { + all, err := loadSpecs() + if err != nil { + t.Fatal(err) + } + d := all["byedpi"].Defaults + if d.FakeTTL != 8 || !d.FakeTTLForced || d.OOBByte != 'a' { + t.Fatalf("byedpi defaults: got %+v", d) + } +} diff --git a/src/convert/tool_test.go b/src/convert/tool_test.go new file mode 100644 index 00000000..c657172d --- /dev/null +++ b/src/convert/tool_test.go @@ -0,0 +1,126 @@ +package convert + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +var coreFiles = []string{ + "convert.go", "spec.go", "extract.go", "getopt.go", + "grammar.go", "ir.go", "parse.go", "emit.go", "tool.go", "detect.go", +} + +func TestCore_MentionsNoToolByName(t *testing.T) { + tools, err := Tools() + if err != nil { + t.Fatal(err) + } + names := make([]string, 0, len(tools)) + for _, x := range tools { + names = append(names, x.Tool) + } + + for _, f := range coreFiles { + raw, err := os.ReadFile(f) + if err != nil { + t.Fatalf("read %s: %v", f, err) + } + body := string(raw) + for _, name := range names { + if f == "extract.go" { + continue + } + if strings.Contains(strings.ToLower(body), name) { + t.Errorf("%s mentions %q; per-tool behaviour belongs in tool_%s.go "+ + "or in rules/%s.json", f, name, name, name) + } + } + } +} + +func TestTools_EachHasItsOwnFileAndRules(t *testing.T) { + tools, err := Tools() + if err != nil { + t.Fatal(err) + } + if len(tools) < 2 { + t.Fatalf("expected at least two tools, got %d", len(tools)) + } + for _, x := range tools { + t.Run(x.Tool, func(t *testing.T) { + for _, f := range []string{ + "tool_" + x.Tool + ".go", + "tool_" + x.Tool + "_test.go", + filepath.Join("rules", x.Tool+".json"), + } { + if _, err := os.Stat(f); err != nil { + t.Errorf("missing %s", f) + } + } + if x.Label == "" { + t.Error("rule file has no label") + } + if len(x.Versions) == 0 { + t.Error("rule file declares no versions") + } + }) + } +} + +func TestTools_RegisteredGrammarsAreNamespaced(t *testing.T) { + tools, err := Tools() + if err != nil { + t.Fatal(err) + } + known := map[string]bool{} + for _, x := range tools { + known[x.Tool] = true + } + for name := range grammars { + tool, _, ok := strings.Cut(name, ".") + if !ok { + continue + } + if !known[tool] { + t.Errorf("grammar %q is namespaced under an unknown tool", name) + } + } +} + +var grammarRefRe = regexp.MustCompile(`"grammar":\s*"([a-zA-Z0-9._]+)"`) + +func TestRules_ReferenceOnlyRegisteredGrammars(t *testing.T) { + entries, err := os.ReadDir("rules") + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + raw, err := os.ReadFile(filepath.Join("rules", e.Name())) + if err != nil { + t.Fatal(err) + } + for _, m := range grammarRefRe.FindAllStringSubmatch(string(raw), -1) { + if _, ok := grammars[m[1]]; !ok { + t.Errorf("%s references unregistered grammar %q", e.Name(), m[1]) + } + } + } +} + +func TestRules_NormalizersAndEmittersResolve(t *testing.T) { + all, err := loadSpecs() + if err != nil { + t.Fatal(err) + } + for _, spec := range all { + if spec.Normalize != "" { + if _, ok := normalizers[spec.Normalize]; !ok { + t.Errorf("%s names normalizer %q, which is not registered", + spec.Tool, spec.Normalize) + } + } + } +} diff --git a/src/convert/grammar_zapret.go b/src/convert/tool_zapret.go similarity index 53% rename from src/convert/grammar_zapret.go rename to src/convert/tool_zapret.go index 43f54454..7016db4b 100644 --- a/src/convert/grammar_zapret.go +++ b/src/convert/tool_zapret.go @@ -4,6 +4,8 @@ import ( "errors" "strconv" "strings" + + "github.com/daniellavrushin/b4/config" ) var zapretDesyncModes = map[string]string{ @@ -50,6 +52,8 @@ var zapretPosMarkers = map[string]struct { } func init() { + registerNormalizer("zapret", normalizeZapret) + registerToolEmitter("zapret", emitZapret) grammars["zapret.desync"] = gZapretDesync grammars["zapret.fooling"] = gZapretFooling grammars["zapret.splitpos"] = gZapretSplitPos @@ -259,3 +263,186 @@ func gZapretWSize(raw string, _ grammarCtx) (Value, error) { } return Value{Int: n, Str: raw}, nil } + +var zapretDroppedModes = map[string]bool{ + "udplen": true, "tamper": true, "hopbyhop": true, "destopt": true, +} + +func emitZapretExtras(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { + if prof.Desync.Mode != "" { + set.TCP.Desync.Mode = prof.Desync.Mode + } + if prof.SynFake.Enabled { + set.TCP.SynFake = true + set.TCP.SynFakeLen = prof.SynFake.Len + } + if prof.Duplicate > 0 { + set.TCP.Duplicate.Enabled = true + set.TCP.Duplicate.Count = clamp(prof.Duplicate, 1, 10) + if tok, ok := ti.first(prof.Index, "dup"); ok { + notes.set(tok, StatusMapped, "duplicateMapped", + "tcp.duplicate.enabled=true", "tcp.duplicate.count="+strconv.Itoa(set.TCP.Duplicate.Count)) + } + } + if prof.SeqOvl.Length > 0 { + set.Fragmentation.SeqOverlapLength = prof.SeqOvl.Length + set.Fragmentation.SeqOverlapPattern = seqOvlPattern(prof.SeqOvl.Pattern) + if tok, ok := ti.first(prof.Index, "seqovl"); ok { + notes.set(tok, StatusMapped, "seqOvlMapped", + "fragmentation.seq_overlap_length="+strconv.Itoa(prof.SeqOvl.Length)) + } + if tok, ok := ti.first(prof.Index, "seqovl_pat"); ok { + notes.set(tok, StatusApproximated, "seqOvlPatternMapped", "fragmentation.seq_overlap_pattern") + } + } + if prof.WinSize > 0 { + set.TCP.Win.Mode = "zero" + if tok, ok := ti.first(prof.Index, "wssize"); ok { + notes.set(tok, StatusApproximated, "wsSizeApproximated", "tcp.win.mode=zero") + } + } + if len(prof.Filters.Excluded) > 0 { + if tok, ok := ti.first(prof.Index, "hostlist_excl_dom", "hostlist_exclude"); ok { + notes.set(tok, StatusUnsupported, "excludeListUnsupported") + } + } + if prof.Skip { + set.Enabled = false + if tok, ok := ti.first(prof.Index, "skip"); ok { + notes.set(tok, StatusMapped, "skipMapped", "enabled=false") + } + } +} + +func noteDesyncModes(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { + tok, ok := ti.first(prof.Index, "desync") + if !ok || len(prof.DesyncModes) == 0 { + return + } + var fields, dropped []string + if set.Fragmentation.Strategy != config.ConfigNone { + fields = append(fields, "fragmentation.strategy="+set.Fragmentation.Strategy) + } + if set.Faking.SNI { + fields = append(fields, "faking.sni=true") + } + if set.TCP.Desync.Mode != config.ConfigOff { + fields = append(fields, "tcp.desync.mode="+set.TCP.Desync.Mode) + } + if set.TCP.SynFake { + fields = append(fields, "tcp.syn_fake=true") + } + if prof.UDP.Present { + fields = append(fields, "udp.mode="+set.UDP.Mode) + } + for _, m := range prof.DesyncModes { + if zapretDroppedModes[m] { + dropped = append(dropped, m) + } + } + if len(dropped) > 0 { + n := notes.set(tok, StatusUnsupported, "desyncModesDropped", fields...) + n.Params = map[string]any{"dropped": strings.Join(dropped, ", ")} + return + } + if len(fields) == 0 { + notes.set(tok, StatusDegenerate, "desyncModesEmpty") + return + } + notes.set(tok, StatusApproximated, "desyncModesMapped", fields...) +} + +func seqOvlPattern(raw string) []string { + hex := strings.TrimPrefix(strings.TrimPrefix(raw, "0x"), "0X") + if hex == "" || len(hex)%2 != 0 { + return []string{"0x16", "0x03", "0x03", "0x00", "0x00"} + } + out := make([]string, 0, len(hex)/2) + for i := 0; i+1 < len(hex); i += 2 { + if !isHex(hex[i]) || !isHex(hex[i+1]) { + return []string{"0x16", "0x03", "0x03", "0x00", "0x00"} + } + out = append(out, "0x"+hex[i:i+2]) + } + return out +} + +func onlyExtSplit(plain, disorder []SplitOp) bool { + ops := append(append([]SplitOp{}, plain...), disorder...) + if len(ops) != 1 { + return false + } + return ops[0].Pos.Anchor == AnchorSNIExt && ops[0].Pos.Offset == 0 +} + +func plainOrDisorder(plain, disorder []SplitOp) SplitOp { + if len(plain) > 0 { + return plain[0] + } + return disorder[0] +} + +func emitZapret(set *config.SetConfig, prof *Profile, ti tokenIndex, notes *noteSet) { + emitZapretExtras(set, prof, ti, notes) + noteDesyncModes(set, prof, ti, notes) +} + +func normalizeZapret(prog *Program, _ []Token, notes *noteSet) { + for _, prof := range prog.Profiles { + normalizeZapretProfile(prof, notes) + promoteUDPFake(prof) + } +} + +func promoteUDPFake(prof *Profile) { + if !prof.UDPOnly() { + return + } + prof.UDP.Present = prof.Fake.Present + prof.UDP.Repeats = prof.Fake.Repeats + prof.UDP.QUICRef = prof.Fake.QUICRef + prof.UDP.TTL = prof.Fake.TTL + prof.UDP.TTLSet = prof.Fake.TTLSet + prof.UDP.Ports = append(prof.UDP.Ports, prof.Filters.UDPPorts...) +} + +func normalizeZapretProfile(prof *Profile, _ *noteSet) { + positions := prof.SplitPositions + token := prof.SplitPosToken + if len(positions) == 0 { + positions = []Pos{{Raw: "1", Offset: 1, Anchor: AnchorAbs, Rel: RelStart}} + token = prof.DesyncToken + } + + for _, mode := range prof.DesyncModes { + switch mode { + case "fake", "fakeknown": + prof.Fake.Present = true + case "rst", "rstack": + prof.Desync.Mode = "rst" + case "synack": + prof.SynFake.Enabled = true + case "syndata": + prof.SynFake.Enabled = true + prof.SynFake.Len = 1 + case "multisplit": + appendSplits(prof, SplitPlain, positions, token) + case "multidisorder": + appendSplits(prof, SplitDisorder, positions, token) + case "fakedsplit", "hostfakesplit": + prof.Fake.Present = true + appendSplits(prof, SplitPlain, positions[:1], token) + case "fakeddisorder": + prof.Fake.Present = true + appendSplits(prof, SplitDisorder, positions[:1], token) + case "ipfrag1", "ipfrag2": + appendSplits(prof, SplitIPFrag, positions[:1], token) + } + } +} + +func appendSplits(prof *Profile, kind SplitKind, positions []Pos, token int) { + for _, p := range positions { + prof.Splits = append(prof.Splits, SplitOp{Kind: kind, Pos: p, Token: token}) + } +} diff --git a/src/convert/zapret_test.go b/src/convert/tool_zapret_test.go similarity index 100% rename from src/convert/zapret_test.go rename to src/convert/tool_zapret_test.go