From 5f5d5137a5169e84d9b417004b78d4b1f2d58e3f Mon Sep 17 00:00:00 2001 From: Nakul Bharti Date: Thu, 10 Sep 2026 22:04:52 +0530 Subject: [PATCH 1/3] perf(jarm): coalesce concurrent fingerprints --- dsl.go | 18 +++++++++------- go.mod | 2 +- jarm.go | 17 +++++++++++++++ jarm_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 jarm.go create mode 100644 jarm_test.go diff --git a/dsl.go b/dsl.go index d8be979..5c3b3f1 100644 --- a/dsl.go +++ b/dsl.go @@ -34,7 +34,6 @@ import ( "strings" "time" - "github.com/projectdiscovery/govaluate" "github.com/Mzack9999/gcache" "github.com/asaskevich/govalidator" "github.com/brianvoe/gofakeit/v7" @@ -48,6 +47,7 @@ import ( "github.com/projectdiscovery/dsl/randomip" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gostruct" + "github.com/projectdiscovery/govaluate" "github.com/projectdiscovery/mapcidr" "github.com/projectdiscovery/utils/conn/connpool" jarm "github.com/projectdiscovery/utils/crypto/jarm" @@ -1497,14 +1497,16 @@ func init() { } // pick the first available proxy from common env vars (case-insensitive) proxy := firstNonEmptyEnv("HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy") - if proxy != "" { - socks5Dialer, err := connpool.NewCreateSOCKS5Dialer(proxy) - if err != nil { - return nil, err + return coalesceJARM(proxy+"\x00"+host, func() (string, error) { + if proxy != "" { + socks5Dialer, err := connpool.NewCreateSOCKS5Dialer(proxy) + if err != nil { + return "", err + } + return jarm.HashWithDialer(socks5Dialer, hostname, port, 10) } - return jarm.HashWithDialer(socks5Dialer, hostname, port, 10) - } - return jarm.HashWithDialer(nil, hostname, port, 10) + return jarm.HashWithDialer(nil, hostname, port, 10) + }) })) MustAddFunction(NewWithSingleSignature("count", diff --git a/go.mod b/go.mod index e6ec819..a18ac1c 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.57.0 // indirect - golang.org/x/sync v0.22.0 // indirect + golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 // indirect golang.org/x/tools v0.47.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/jarm.go b/jarm.go new file mode 100644 index 0000000..17f9b80 --- /dev/null +++ b/jarm.go @@ -0,0 +1,17 @@ +package dsl + +import "golang.org/x/sync/singleflight" + +var jarmFingerprints singleflight.Group + +// coalesceJARM suppresses only overlapping calculations. Results are not kept +// after the active callers return, so separate scans cannot observe stale data. +func coalesceJARM(key string, fingerprint func() (string, error)) (string, error) { + value, err, _ := jarmFingerprints.Do(key, func() (interface{}, error) { + return fingerprint() + }) + if err != nil { + return "", err + } + return value.(string), nil +} diff --git a/jarm_test.go b/jarm_test.go new file mode 100644 index 0000000..dada837 --- /dev/null +++ b/jarm_test.go @@ -0,0 +1,59 @@ +package dsl + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestCoalesceJARMOnlyRunsOverlappingFingerprintOnce(t *testing.T) { + firstStarted := make(chan struct{}) + releaseFirst := make(chan struct{}) + firstDone := make(chan struct { + value string + err error + }, 1) + var calls atomic.Int32 + + go func() { + value, err := coalesceJARM("proxy\x00example.test:443", func() (string, error) { + calls.Add(1) + close(firstStarted) + <-releaseFirst + return "fingerprint", nil + }) + firstDone <- struct { + value string + err error + }{value: value, err: err} + }() + + <-firstStarted + time.AfterFunc(10*time.Millisecond, func() { close(releaseFirst) }) + value, err := coalesceJARM("proxy\x00example.test:443", func() (string, error) { + calls.Add(1) + return "duplicate", nil + }) + + require.NoError(t, err) + require.Equal(t, "fingerprint", value) + first := <-firstDone + require.NoError(t, first.err) + require.Equal(t, "fingerprint", first.value) + require.EqualValues(t, 1, calls.Load()) +} + +func TestCoalesceJARMDoesNotCacheCompletedFingerprint(t *testing.T) { + var calls atomic.Int32 + for range 2 { + value, err := coalesceJARM("direct\x00example.test:443", func() (string, error) { + calls.Add(1) + return "fingerprint", nil + }) + require.NoError(t, err) + require.Equal(t, "fingerprint", value) + } + require.EqualValues(t, 2, calls.Load()) +} From 8e1207b367b80474ca496d92f276d69de001fbff Mon Sep 17 00:00:00 2001 From: Nakul Bharti Date: Thu, 10 Sep 2026 23:19:14 +0530 Subject: [PATCH 2/3] fix(jarm): honor configured SOCKS5 route --- dsl.go | 2 +- jarm.go | 10 ++++++++++ jarm_test.go | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/dsl.go b/dsl.go index 5c3b3f1..426d186 100644 --- a/dsl.go +++ b/dsl.go @@ -1496,7 +1496,7 @@ func init() { return nil, err } // pick the first available proxy from common env vars (case-insensitive) - proxy := firstNonEmptyEnv("HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy") + proxy := jarmProxyFromEnvironment() return coalesceJARM(proxy+"\x00"+host, func() (string, error) { if proxy != "" { socks5Dialer, err := connpool.NewCreateSOCKS5Dialer(proxy) diff --git a/jarm.go b/jarm.go index 17f9b80..a1a9d7c 100644 --- a/jarm.go +++ b/jarm.go @@ -4,6 +4,16 @@ import "golang.org/x/sync/singleflight" var jarmFingerprints singleflight.Group +func jarmProxyFromEnvironment() string { + // Nuclei and Aurora expose their SOCKS5 scan route using SOCKS5_PROXY. + // Keep the previous HTTP-family variables as fallbacks for callers that + // already place a socks5:// URL there. + return firstNonEmptyEnv( + "SOCKS5_PROXY", "socks5_proxy", + "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", + ) +} + // coalesceJARM suppresses only overlapping calculations. Results are not kept // after the active callers return, so separate scans cannot observe stale data. func coalesceJARM(key string, fingerprint func() (string, error)) (string, error) { diff --git a/jarm_test.go b/jarm_test.go index dada837..9d9d313 100644 --- a/jarm_test.go +++ b/jarm_test.go @@ -57,3 +57,25 @@ func TestCoalesceJARMDoesNotCacheCompletedFingerprint(t *testing.T) { } require.EqualValues(t, 2, calls.Load()) } + +func TestJARMProxyPrefersConfiguredSOCKS5Route(t *testing.T) { + t.Setenv("SOCKS5_PROXY", "socks5://scan-route.example:1080") + t.Setenv("socks5_proxy", "socks5://lowercase.example:1080") + t.Setenv("HTTP_PROXY", "socks5://legacy-http.example:1080") + t.Setenv("http_proxy", "socks5://legacy-http-lower.example:1080") + t.Setenv("HTTPS_PROXY", "socks5://legacy-https.example:1080") + t.Setenv("https_proxy", "socks5://legacy-https-lower.example:1080") + + require.Equal(t, "socks5://scan-route.example:1080", jarmProxyFromEnvironment()) +} + +func TestJARMProxyRetainsLegacyFallback(t *testing.T) { + t.Setenv("SOCKS5_PROXY", "") + t.Setenv("socks5_proxy", "") + t.Setenv("HTTP_PROXY", "socks5://legacy.example:1080") + t.Setenv("http_proxy", "") + t.Setenv("HTTPS_PROXY", "") + t.Setenv("https_proxy", "") + + require.Equal(t, "socks5://legacy.example:1080", jarmProxyFromEnvironment()) +} From d841f3792c1c36de43631b6933670de84aa4f711 Mon Sep 17 00:00:00 2001 From: Nakul Bharti Date: Sat, 12 Sep 2026 03:43:32 +0530 Subject: [PATCH 3/3] test(jarm): handle case-insensitive Windows environment --- jarm_test.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/jarm_test.go b/jarm_test.go index 9d9d313..57675e7 100644 --- a/jarm_test.go +++ b/jarm_test.go @@ -1,6 +1,7 @@ package dsl import ( + "runtime" "sync/atomic" "testing" "time" @@ -60,22 +61,28 @@ func TestCoalesceJARMDoesNotCacheCompletedFingerprint(t *testing.T) { func TestJARMProxyPrefersConfiguredSOCKS5Route(t *testing.T) { t.Setenv("SOCKS5_PROXY", "socks5://scan-route.example:1080") - t.Setenv("socks5_proxy", "socks5://lowercase.example:1080") t.Setenv("HTTP_PROXY", "socks5://legacy-http.example:1080") - t.Setenv("http_proxy", "socks5://legacy-http-lower.example:1080") t.Setenv("HTTPS_PROXY", "socks5://legacy-https.example:1080") - t.Setenv("https_proxy", "socks5://legacy-https-lower.example:1080") + if runtime.GOOS != "windows" { + // Windows environment variable names are case-insensitive, so setting + // these aliases would overwrite the uppercase variables above. + t.Setenv("socks5_proxy", "socks5://lowercase.example:1080") + t.Setenv("http_proxy", "socks5://legacy-http-lower.example:1080") + t.Setenv("https_proxy", "socks5://legacy-https-lower.example:1080") + } require.Equal(t, "socks5://scan-route.example:1080", jarmProxyFromEnvironment()) } func TestJARMProxyRetainsLegacyFallback(t *testing.T) { t.Setenv("SOCKS5_PROXY", "") - t.Setenv("socks5_proxy", "") t.Setenv("HTTP_PROXY", "socks5://legacy.example:1080") - t.Setenv("http_proxy", "") t.Setenv("HTTPS_PROXY", "") - t.Setenv("https_proxy", "") + if runtime.GOOS != "windows" { + t.Setenv("socks5_proxy", "") + t.Setenv("http_proxy", "") + t.Setenv("https_proxy", "") + } require.Equal(t, "socks5://legacy.example:1080", jarmProxyFromEnvironment()) }