mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-08-18 13:33:49 +00:00
Add zapret tool implementation and associated tests
- Implemented the zapret tool in `tool_zapret.go`, including desynchronization modes, fooling strategies, and split position parsing. - Added grammar definitions for zapret configurations. - Created comprehensive tests in `tool_zapret_test.go` to validate the functionality of zapret options, ensuring all recognized options are reported and conflicts are handled correctly. - Included tests for desync modes, fooling strategies, and the handling of UDP profiles.
This commit is contained in:
parent
1ceaab7aa9
commit
37d05fb102
18 changed files with 2962 additions and 1286 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <n>, <n>+s, <n>+h or <n>+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 <offset>[: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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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"`
|
||||
|
|
|
|||
28
src/convert/tool.go
Normal file
28
src/convert/tool.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
149
src/convert/tool_byedpi.go
Normal file
149
src/convert/tool_byedpi.go
Normal file
|
|
@ -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 <n>, <n>+s, <n>+h or <n>+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 <offset>[: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
|
||||
}
|
||||
738
src/convert/tool_byedpi_test.go
Normal file
738
src/convert/tool_byedpi_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
126
src/convert/tool_test.go
Normal file
126
src/convert/tool_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue