Add relay header profiles and randomized request timing/batching

This commit is contained in:
Amin.MasterkinG
2026-04-21 10:00:33 +03:30
parent 44e85467f9
commit af5639d691
5 changed files with 236 additions and 85 deletions
+62 -17
View File
@@ -42,23 +42,13 @@ func (b *relayHeaderBuilder) Apply(req *http.Request) {
req.Header.Set("User-Agent", ua)
}
if b.cfg.HTTPHeaderProfile == "browser" {
req.Header.Set("Accept", pickRandomString(
"*/*",
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"application/json,text/plain,*/*",
))
req.Header.Set("Accept-Language", b.pickAcceptLanguage())
req.Header.Set("Cache-Control", pickRandomString("no-cache", "max-age=0", "no-store"))
req.Header.Set("Pragma", "no-cache")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", pickRandomString("same-origin", "same-site", "cross-site"))
req.Header.Set("Priority", pickRandomString("u=0, i", "u=1, i"))
if referer := b.pickReferer(); referer != "" {
req.Header.Set("Referer", referer)
}
switch b.cfg.HTTPHeaderProfile {
case "browser":
b.applyBrowserProfile(req)
case "cdn":
b.applyCDNProfile(req)
case "api":
b.applyAPIProfile(req)
}
if b.cfg.HTTPRandomizeHeaders {
@@ -75,6 +65,57 @@ func (b *relayHeaderBuilder) Apply(req *http.Request) {
}
}
func (b *relayHeaderBuilder) applyBrowserProfile(req *http.Request) {
req.Header.Set("Accept", pickRandomString(
"*/*",
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"application/json,text/plain,*/*",
))
req.Header.Set("Accept-Language", b.pickAcceptLanguage())
req.Header.Set("Cache-Control", pickRandomString("no-cache", "max-age=0", "no-store"))
req.Header.Set("Pragma", "no-cache")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", pickRandomString("same-origin", "same-site", "cross-site"))
req.Header.Set("Priority", pickRandomString("u=0, i", "u=1, i"))
if maybeTrue() {
req.Header.Set("DNT", "1")
}
if referer := b.pickReferer(); referer != "" {
req.Header.Set("Referer", referer)
}
}
func (b *relayHeaderBuilder) applyCDNProfile(req *http.Request) {
req.Header.Set("Accept", pickRandomString("*/*", "application/octet-stream,*/*", "application/json,*/*"))
req.Header.Set("Accept-Language", b.pickAcceptLanguage())
req.Header.Set("Cache-Control", pickRandomString("no-store", "no-cache", "max-age=0"))
req.Header.Set("Pragma", "no-cache")
req.Header.Set("X-Requested-With", pickRandomString("XMLHttpRequest", "Fetch"))
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", pickRandomString("cors", "same-origin"))
req.Header.Set("Sec-Fetch-Site", pickRandomString("same-origin", "same-site"))
if referer := b.pickReferer(); referer != "" {
req.Header.Set("Referer", referer)
}
}
func (b *relayHeaderBuilder) applyAPIProfile(req *http.Request) {
req.Header.Set("Accept", pickRandomString("application/json", "application/octet-stream", "application/json,text/plain,*/*"))
req.Header.Set("Cache-Control", pickRandomString("no-store", "no-cache"))
req.Header.Set("Pragma", "no-cache")
req.Header.Set("X-Requested-With", pickRandomString("XMLHttpRequest", "APIClient"))
if maybeTrue() {
req.Header.Set("X-Requested-At", randomHex(6))
}
if b.cfg.HTTPAcceptLanguage != "" {
req.Header.Set("Accept-Language", b.pickAcceptLanguage())
}
if referer := b.pickReferer(); referer != "" && maybeTrue() {
req.Header.Set("Referer", referer)
}
}
func (b *relayHeaderBuilder) pickUserAgent() string {
if len(b.userAgents) == 0 {
return ""
@@ -225,6 +266,10 @@ func randomPadding(minBytes int, maxBytes int) string {
return padding
}
func maybeTrue() bool {
return randomIndex(2) == 0
}
func randomHex(byteCount int) string {
if byteCount <= 0 {
return ""
+47 -10
View File
@@ -58,14 +58,14 @@ func (w *sendWorker) run(ctx context.Context, c *Client) {
c.reclaimExpiredInFlight()
batch, selected := c.buildNextBatch()
if len(batch.Packets) == 0 {
c.waitForSendWork(ctx, pollInterval)
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
continue
}
if err := batch.Validate(); err != nil {
c.log.Errorf("<red>worker=<cyan>%d</cyan> invalid batch: <cyan>%v</cyan></red>", w.id, err)
c.requeueSelected(selected)
c.waitForSendWork(ctx, pollInterval)
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
continue
}
@@ -75,14 +75,14 @@ func (w *sendWorker) run(ctx context.Context, c *Client) {
if err != nil {
c.log.Errorf("<red>worker=<cyan>%d</cyan> encrypt batch failed: <cyan>%v</cyan></red>", w.id, err)
c.requeueSelected(selected)
c.waitForSendWork(ctx, pollInterval)
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
continue
}
if err := w.postBatch(ctx, c, batch, body); err != nil {
c.log.Warnf("<yellow>worker=<cyan>%d</cyan> send failed for batch=<cyan>%s</cyan>: <cyan>%v</cyan></yellow>", w.id, batch.BatchID, err)
c.requeueSelected(selected)
c.waitForSendWork(ctx, pollInterval)
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
continue
}
@@ -114,16 +114,17 @@ func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
if len(connections) > 1 {
start = int(c.batchCursor.Add(1)-1) % len(connections)
}
maxPackets, maxBatchBytes := c.effectiveBatchLimits()
selected := make([]dequeuedPacket, 0, c.cfg.MaxPacketsPerBatch)
packets := make([]protocol.Packet, 0, c.cfg.MaxPacketsPerBatch)
selected := make([]dequeuedPacket, 0, maxPackets)
packets := make([]protocol.Packet, 0, maxPackets)
totalBytes := 0
for len(selected) < c.cfg.MaxPacketsPerBatch {
for len(selected) < maxPackets {
progress := false
for offset := range connections {
if len(selected) >= c.cfg.MaxPacketsPerBatch {
if len(selected) >= maxPackets {
break
}
@@ -135,7 +136,7 @@ func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
}
packetBytes := len(item.Packet.Payload)
if len(selected) > 0 && totalBytes+packetBytes > c.cfg.MaxBatchBytes {
if len(selected) > 0 && totalBytes+packetBytes > maxBatchBytes {
socksConn.RequeueFront([]*SOCKSOutboundQueueItem{item})
continue
}
@@ -173,7 +174,7 @@ func (c *Client) buildPollBatch(connections []*SOCKSConnection) (protocol.Batch,
now := time.Now()
nowUnixMS := now.UnixMilli()
lastUnixMS := c.lastPollUnixMS.Load()
minInterval := time.Duration(c.cfg.IdlePollIntervalMS) * time.Millisecond
minInterval := c.jitterDuration(time.Duration(c.cfg.IdlePollIntervalMS) * time.Millisecond)
if lastUnixMS > 0 && nowUnixMS-lastUnixMS < minInterval.Milliseconds() {
return protocol.Batch{}, false
}
@@ -188,6 +189,39 @@ func (c *Client) buildPollBatch(connections []*SOCKSConnection) (protocol.Batch,
return batch, true
}
func (c *Client) effectiveBatchLimits() (int, int) {
maxPackets := c.cfg.MaxPacketsPerBatch
maxBatchBytes := c.cfg.MaxBatchBytes
if !c.cfg.HTTPBatchRandomize {
return maxPackets, maxBatchBytes
}
if c.cfg.HTTPBatchPacketsJitter > 0 && maxPackets > 1 {
delta := randomIndex(c.cfg.HTTPBatchPacketsJitter + 1)
if adjusted := maxPackets - delta; adjusted >= 1 {
maxPackets = adjusted
}
}
if c.cfg.HTTPBatchBytesJitter > 0 && maxBatchBytes > c.cfg.MaxChunkSize {
delta := randomIndex(c.cfg.HTTPBatchBytesJitter + 1)
if adjusted := maxBatchBytes - delta; adjusted >= c.cfg.MaxChunkSize {
maxBatchBytes = adjusted
}
}
return maxPackets, maxBatchBytes
}
func (c *Client) jitterDuration(base time.Duration) time.Duration {
if base <= 0 || c.cfg.HTTPTimingJitterMS <= 0 {
return base
}
jitter := time.Duration(randomIndex(c.cfg.HTTPTimingJitterMS+1)) * time.Millisecond
return base + jitter
}
func (c *Client) requeueSelected(selected []dequeuedPacket) {
grouped := make(map[*SOCKSConnection][]string)
for _, entry := range selected {
@@ -343,15 +377,18 @@ func (c *Client) applyResponsePacket(packet protocol.Packet) error {
protocol.PacketTypeSOCKSAuthFailed,
protocol.PacketTypeSOCKSUpstreamUnavailable:
message := packet.Type.String()
if len(packet.Payload) > 0 {
message = string(packet.Payload)
}
_ = socksConn.AckPacket(packet)
socksConn.ConnectFailure = message
c.log.Warnf(
"<yellow>connect failure applied socks_id=<cyan>%d</cyan> reason=<cyan>%s</cyan></yellow>",
socksConn.ID, message,
)
socksConn.CompleteConnect(fmt.Errorf("%s", message))
_ = socksConn.CloseLocal()
return nil
+1
View File
@@ -73,6 +73,7 @@ func TestBuildNextBatchRotatesAcrossConnections(t *testing.T) {
MaxBatchBytes: 4096,
WorkerCount: 1,
MaxQueueBytesPerSOCKS: 4096,
HTTPBatchRandomize: false,
}
client := New(cfg, nil)
+122 -58
View File
@@ -16,67 +16,75 @@ import (
)
type Config struct {
AESEncryptionKey string
RelayURL string
HTTPUserAgentsFile string
HTTPHeaderProfile string
HTTPRandomizeHeaders bool
HTTPPaddingHeader string
HTTPPaddingMinBytes int
HTTPPaddingMaxBytes int
HTTPReferer string
HTTPAcceptLanguage string
ServerHost string
ServerPort int
SOCKSHost string
SOCKSPort int
SOCKSAuth bool
SOCKSUsername string
SOCKSPassword string
LogLevel string
MaxChunkSize int
MaxPacketsPerBatch int
MaxBatchBytes int
WorkerCount int
HTTPRequestTimeoutMS int
WorkerPollIntervalMS int
IdlePollIntervalMS int
MaxQueueBytesPerSOCKS int
AckTimeoutMS int
MaxRetryCount int
SessionIdleTimeoutMS int
SOCKSIdleTimeoutMS int
ReadBodyLimitBytes int
MaxServerQueueBytes int
AESEncryptionKey string
RelayURL string
HTTPUserAgentsFile string
HTTPHeaderProfile string
HTTPRandomizeHeaders bool
HTTPPaddingHeader string
HTTPPaddingMinBytes int
HTTPPaddingMaxBytes int
HTTPReferer string
HTTPAcceptLanguage string
HTTPTimingJitterMS int
HTTPBatchRandomize bool
HTTPBatchPacketsJitter int
HTTPBatchBytesJitter int
ServerHost string
ServerPort int
SOCKSHost string
SOCKSPort int
SOCKSAuth bool
SOCKSUsername string
SOCKSPassword string
LogLevel string
MaxChunkSize int
MaxPacketsPerBatch int
MaxBatchBytes int
WorkerCount int
HTTPRequestTimeoutMS int
WorkerPollIntervalMS int
IdlePollIntervalMS int
MaxQueueBytesPerSOCKS int
AckTimeoutMS int
MaxRetryCount int
SessionIdleTimeoutMS int
SOCKSIdleTimeoutMS int
ReadBodyLimitBytes int
MaxServerQueueBytes int
}
func Load(path string) (Config, error) {
cfg := Config{
SOCKSHost: "127.0.0.1",
SOCKSPort: 1080,
HTTPUserAgentsFile: "user-agents.txt",
HTTPHeaderProfile: "browser",
HTTPRandomizeHeaders: true,
HTTPPaddingHeader: "X-Padding",
HTTPPaddingMinBytes: 16,
HTTPPaddingMaxBytes: 48,
ServerHost: "127.0.0.1",
ServerPort: 28080,
LogLevel: "INFO",
MaxChunkSize: 16 * 1024,
MaxPacketsPerBatch: 32,
MaxBatchBytes: 256 * 1024,
WorkerCount: 4,
HTTPRequestTimeoutMS: 15000,
WorkerPollIntervalMS: 200,
IdlePollIntervalMS: 1000,
MaxQueueBytesPerSOCKS: 1024 * 1024,
AckTimeoutMS: 5000,
MaxRetryCount: 5,
SessionIdleTimeoutMS: 5 * 60 * 1000,
SOCKSIdleTimeoutMS: 2 * 60 * 1000,
ReadBodyLimitBytes: 2 * 1024 * 1024,
MaxServerQueueBytes: 2 * 1024 * 1024,
SOCKSHost: "127.0.0.1",
SOCKSPort: 1080,
HTTPUserAgentsFile: "user-agents.txt",
HTTPHeaderProfile: "browser",
HTTPRandomizeHeaders: true,
HTTPPaddingHeader: "X-Padding",
HTTPPaddingMinBytes: 16,
HTTPPaddingMaxBytes: 48,
HTTPTimingJitterMS: 50,
HTTPBatchRandomize: true,
HTTPBatchPacketsJitter: 4,
HTTPBatchBytesJitter: 32768,
ServerHost: "127.0.0.1",
ServerPort: 28080,
LogLevel: "INFO",
MaxChunkSize: 16 * 1024,
MaxPacketsPerBatch: 32,
MaxBatchBytes: 256 * 1024,
WorkerCount: 4,
HTTPRequestTimeoutMS: 15000,
WorkerPollIntervalMS: 200,
IdlePollIntervalMS: 1000,
MaxQueueBytesPerSOCKS: 1024 * 1024,
AckTimeoutMS: 5000,
MaxRetryCount: 5,
SessionIdleTimeoutMS: 5 * 60 * 1000,
SOCKSIdleTimeoutMS: 2 * 60 * 1000,
ReadBodyLimitBytes: 2 * 1024 * 1024,
MaxServerQueueBytes: 2 * 1024 * 1024,
}
file, err := os.Open(path)
@@ -114,6 +122,7 @@ func Load(path string) (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_RANDOMIZE_HEADERS: %w", err)
}
cfg.HTTPRandomizeHeaders = randomize
case "HTTP_PADDING_HEADER":
cfg.HTTPPaddingHeader = trimString(value)
@@ -122,17 +131,47 @@ func Load(path string) (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_PADDING_MIN_BYTES: %w", err)
}
cfg.HTTPPaddingMinBytes = size
case "HTTP_PADDING_MAX_BYTES":
size, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_PADDING_MAX_BYTES: %w", err)
}
cfg.HTTPPaddingMaxBytes = size
case "HTTP_REFERER":
cfg.HTTPReferer = trimString(value)
case "HTTP_ACCEPT_LANGUAGE":
cfg.HTTPAcceptLanguage = trimString(value)
case "HTTP_TIMING_JITTER_MS":
valueInt, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_TIMING_JITTER_MS: %w", err)
}
cfg.HTTPTimingJitterMS = valueInt
case "HTTP_BATCH_RANDOMIZE":
randomize, err := strconv.ParseBool(value)
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_BATCH_RANDOMIZE: %w", err)
}
cfg.HTTPBatchRandomize = randomize
case "HTTP_BATCH_PACKETS_JITTER":
valueInt, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_BATCH_PACKETS_JITTER: %w", err)
}
cfg.HTTPBatchPacketsJitter = valueInt
case "HTTP_BATCH_BYTES_JITTER":
valueInt, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_BATCH_BYTES_JITTER: %w", err)
}
cfg.HTTPBatchBytesJitter = valueInt
case "SERVER_HOST":
cfg.ServerHost = trimString(value)
case "SERVER_PORT":
@@ -140,6 +179,7 @@ func Load(path string) (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("parse SERVER_PORT: %w", err)
}
cfg.ServerPort = port
case "SOCKS_HOST":
cfg.SOCKSHost = trimString(value)
@@ -148,12 +188,14 @@ func Load(path string) (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("parse SOCKS_PORT: %w", err)
}
cfg.SOCKSPort = port
case "SOCKS_AUTH":
auth, err := strconv.ParseBool(value)
if err != nil {
return Config{}, fmt.Errorf("parse SOCKS_AUTH: %w", err)
}
cfg.SOCKSAuth = auth
case "SOCKS_USERNAME":
cfg.SOCKSUsername = trimString(value)
@@ -166,60 +208,70 @@ func Load(path string) (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("parse MAX_CHUNK_SIZE: %w", err)
}
cfg.MaxChunkSize = size
case "MAX_PACKETS_PER_BATCH":
count, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse MAX_PACKETS_PER_BATCH: %w", err)
}
cfg.MaxPacketsPerBatch = count
case "MAX_BATCH_BYTES":
size, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse MAX_BATCH_BYTES: %w", err)
}
cfg.MaxBatchBytes = size
case "WORKER_COUNT":
count, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse WORKER_COUNT: %w", err)
}
cfg.WorkerCount = count
case "HTTP_REQUEST_TIMEOUT_MS":
timeout, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_REQUEST_TIMEOUT_MS: %w", err)
}
cfg.HTTPRequestTimeoutMS = timeout
case "WORKER_POLL_INTERVAL_MS":
interval, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse WORKER_POLL_INTERVAL_MS: %w", err)
}
cfg.WorkerPollIntervalMS = interval
case "IDLE_POLL_INTERVAL_MS":
interval, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse IDLE_POLL_INTERVAL_MS: %w", err)
}
cfg.IdlePollIntervalMS = interval
case "MAX_QUEUE_BYTES_PER_SOCKS":
size, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse MAX_QUEUE_BYTES_PER_SOCKS: %w", err)
}
cfg.MaxQueueBytesPerSOCKS = size
case "ACK_TIMEOUT_MS":
timeout, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse ACK_TIMEOUT_MS: %w", err)
}
cfg.AckTimeoutMS = timeout
case "MAX_RETRY_COUNT":
count, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse MAX_RETRY_COUNT: %w", err)
}
cfg.MaxRetryCount = count
case "SESSION_IDLE_TIMEOUT_MS":
timeout, err := strconv.Atoi(value)
@@ -232,18 +284,21 @@ func Load(path string) (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("parse SOCKS_IDLE_TIMEOUT_MS: %w", err)
}
cfg.SOCKSIdleTimeoutMS = timeout
case "READ_BODY_LIMIT_BYTES":
size, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse READ_BODY_LIMIT_BYTES: %w", err)
}
cfg.ReadBodyLimitBytes = size
case "MAX_SERVER_QUEUE_BYTES":
size, err := strconv.Atoi(value)
if err != nil {
return Config{}, fmt.Errorf("parse MAX_SERVER_QUEUE_BYTES: %w", err)
}
cfg.MaxServerQueueBytes = size
}
}
@@ -292,7 +347,7 @@ func (c Config) ValidateClient() error {
return fmt.Errorf("invalid MAX_RETRY_COUNT: %d", c.MaxRetryCount)
}
if c.HTTPHeaderProfile != "browser" && c.HTTPHeaderProfile != "minimal" {
if c.HTTPHeaderProfile != "browser" && c.HTTPHeaderProfile != "cdn" && c.HTTPHeaderProfile != "api" && c.HTTPHeaderProfile != "minimal" {
return fmt.Errorf("invalid HTTP_HEADER_PROFILE: %s", c.HTTPHeaderProfile)
}
@@ -303,6 +358,15 @@ func (c Config) ValidateClient() error {
if c.HTTPPaddingMaxBytes < c.HTTPPaddingMinBytes {
return fmt.Errorf("HTTP_PADDING_MAX_BYTES must be >= HTTP_PADDING_MIN_BYTES")
}
if c.HTTPTimingJitterMS < 0 {
return fmt.Errorf("invalid HTTP_TIMING_JITTER_MS: %d", c.HTTPTimingJitterMS)
}
if c.HTTPBatchPacketsJitter < 0 {
return fmt.Errorf("invalid HTTP_BATCH_PACKETS_JITTER: %d", c.HTTPBatchPacketsJitter)
}
if c.HTTPBatchBytesJitter < 0 {
return fmt.Errorf("invalid HTTP_BATCH_BYTES_JITTER: %d", c.HTTPBatchBytesJitter)
}
if c.MaxQueueBytesPerSOCKS < c.MaxChunkSize {
return fmt.Errorf("MAX_QUEUE_BYTES_PER_SOCKS must be >= MAX_CHUNK_SIZE")