Add randomized relay headers and configurable user-agent rotation

This commit is contained in:
Amin.MasterkinG
2026-04-21 09:52:40 +03:30
parent 9260230a35
commit 44e85467f9
6 changed files with 354 additions and 1 deletions
+9
View File
@@ -2,6 +2,15 @@
AES_ENCRYPTION_KEY = "c4710a45afed2fdc00e0522c70802e71" AES_ENCRYPTION_KEY = "c4710a45afed2fdc00e0522c70802e71"
RELAY_URL = "http://127.0.0.1/relay.php" RELAY_URL = "http://127.0.0.1/relay.php"
# ============================================================================== # ==============================================================================
HTTP_USER_AGENTS_FILE = "user-agents.txt"
HTTP_HEADER_PROFILE = "browser"
HTTP_RANDOMIZE_HEADERS = true
HTTP_PADDING_HEADER = "X-Padding"
HTTP_PADDING_MIN_BYTES = 16
HTTP_PADDING_MAX_BYTES = 48
HTTP_REFERER = ""
HTTP_ACCEPT_LANGUAGE = ""
# ==============================================================================
LOG_LEVEL = "DEBUG" LOG_LEVEL = "DEBUG"
# ============================================================================== # ==============================================================================
SOCKS_HOST = "127.0.0.1" SOCKS_HOST = "127.0.0.1"
+2
View File
@@ -26,6 +26,7 @@ type Client struct {
clientSessionKey string clientSessionKey string
socksConnections *SOCKSConnectionStore socksConnections *SOCKSConnectionStore
chunkPolicy ChunkPolicy chunkPolicy ChunkPolicy
headerBuilder *relayHeaderBuilder
connMu sync.Mutex connMu sync.Mutex
conns map[net.Conn]struct{} conns map[net.Conn]struct{}
@@ -44,6 +45,7 @@ func New(cfg config.Config, lg *logger.Logger) *Client {
clientSessionKey: clientSessionKey, clientSessionKey: clientSessionKey,
socksConnections: NewSOCKSConnectionStore(), socksConnections: NewSOCKSConnectionStore(),
chunkPolicy: newChunkPolicy(cfg), chunkPolicy: newChunkPolicy(cfg),
headerBuilder: newRelayHeaderBuilder(cfg, lg),
conns: make(map[net.Conn]struct{}), conns: make(map[net.Conn]struct{}),
workCh: make(chan struct{}, 1), workCh: make(chan struct{}, 1),
} }
+239
View File
@@ -0,0 +1,239 @@
// ==============================================================================
// MasterHttpRelayVPN
// Author: MasterkinG32
// Github: https://github.com/masterking32
// Year: 2026
// ==============================================================================
package client
import (
"bufio"
"crypto/rand"
"encoding/hex"
"math/big"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"masterhttprelayvpn/internal/config"
"masterhttprelayvpn/internal/logger"
)
type relayHeaderBuilder struct {
cfg config.Config
userAgents []string
refererCandidates []string
}
func newRelayHeaderBuilder(cfg config.Config, log *logger.Logger) *relayHeaderBuilder {
builder := &relayHeaderBuilder{
cfg: cfg,
userAgents: loadUserAgents(cfg.HTTPUserAgentsFile, log),
}
builder.refererCandidates = buildRefererCandidates(cfg)
return builder
}
func (b *relayHeaderBuilder) Apply(req *http.Request) {
if ua := b.pickUserAgent(); ua != "" {
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)
}
}
if b.cfg.HTTPRandomizeHeaders {
padding := randomPadding(b.cfg.HTTPPaddingMinBytes, b.cfg.HTTPPaddingMaxBytes)
if padding != "" {
headerName := strings.TrimSpace(b.cfg.HTTPPaddingHeader)
if headerName == "" {
headerName = "X-Padding"
}
req.Header.Set(headerName, padding)
}
req.Header.Set("X-Request-Nonce", randomHex(8))
}
}
func (b *relayHeaderBuilder) pickUserAgent() string {
if len(b.userAgents) == 0 {
return ""
}
return b.userAgents[randomIndex(len(b.userAgents))]
}
func (b *relayHeaderBuilder) pickReferer() string {
if len(b.refererCandidates) == 0 {
return ""
}
return b.refererCandidates[randomIndex(len(b.refererCandidates))]
}
func (b *relayHeaderBuilder) pickAcceptLanguage() string {
if strings.TrimSpace(b.cfg.HTTPAcceptLanguage) != "" {
return b.cfg.HTTPAcceptLanguage
}
return pickRandomString(
"en-US,en;q=0.9",
"en-GB,en;q=0.9",
"fa-IR,fa;q=0.9,en-US;q=0.7,en;q=0.6",
"de-DE,de;q=0.9,en-US;q=0.7,en;q=0.6",
)
}
func loadUserAgents(path string, log *logger.Logger) []string {
userAgents := readUserAgentsFromFile(path)
if len(userAgents) > 0 {
return userAgents
}
if log != nil {
log.Warnf("<yellow>user agents file <cyan>%s</cyan> not found or empty, using built-in defaults</yellow>", path)
}
return []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
}
}
func readUserAgentsFromFile(path string) []string {
if strings.TrimSpace(path) == "" {
return nil
}
candidates := []string{path}
if !filepath.IsAbs(path) {
candidates = append(candidates, filepath.Join(".", path))
}
for _, candidate := range candidates {
file, err := os.Open(candidate)
if err != nil {
continue
}
userAgents := make([]string, 0, 16)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
userAgents = append(userAgents, line)
}
if len(userAgents) > 0 {
_ = file.Close()
return userAgents
}
_ = file.Close()
}
return nil
}
func buildRefererCandidates(cfg config.Config) []string {
if strings.TrimSpace(cfg.HTTPReferer) != "" {
return []string{cfg.HTTPReferer}
}
relayURL, err := url.Parse(cfg.RelayURL)
if err != nil || relayURL.Scheme == "" || relayURL.Host == "" {
return nil
}
base := relayURL.Scheme + "://" + relayURL.Host
return []string{
base + "/",
base + "/index.html",
base + "/home",
base + "/api/status",
}
}
func pickRandomString(values ...string) string {
if len(values) == 0 {
return ""
}
return values[randomIndex(len(values))]
}
func randomIndex(length int) int {
if length <= 1 {
return 0
}
n, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
if err != nil {
return 0
}
return int(n.Int64())
}
func randomPadding(minBytes int, maxBytes int) string {
if maxBytes <= 0 || maxBytes < minBytes {
return ""
}
length := minBytes
if maxBytes > minBytes {
length += randomIndex(maxBytes - minBytes + 1)
}
if length <= 0 {
return ""
}
raw := make([]byte, (length+1)/2)
if _, err := rand.Read(raw); err != nil {
return ""
}
padding := hex.EncodeToString(raw)
if len(padding) > length {
padding = padding[:length]
}
return padding
}
func randomHex(byteCount int) string {
if byteCount <= 0 {
return ""
}
raw := make([]byte, byteCount)
if _, err := rand.Read(raw); err != nil {
return ""
}
return hex.EncodeToString(raw)
}
+17 -1
View File
@@ -122,7 +122,7 @@ func (c *Client) buildNextBatch() (protocol.Batch, []dequeuedPacket) {
for len(selected) < c.cfg.MaxPacketsPerBatch { for len(selected) < c.cfg.MaxPacketsPerBatch {
progress := false progress := false
for offset := 0; offset < len(connections); offset++ { for offset := range connections {
if len(selected) >= c.cfg.MaxPacketsPerBatch { if len(selected) >= c.cfg.MaxPacketsPerBatch {
break break
} }
@@ -197,6 +197,7 @@ func (c *Client) requeueSelected(selected []dequeuedPacket) {
for socksConn, identityKeys := range grouped { for socksConn, identityKeys := range grouped {
socksConn.RequeueInFlightByIdentity(identityKeys) socksConn.RequeueInFlightByIdentity(identityKeys)
} }
if len(grouped) > 0 { if len(grouped) > 0 {
c.signalSendWork() c.signalSendWork()
} }
@@ -207,6 +208,7 @@ func (c *Client) markSelectedInFlight(selected []dequeuedPacket) {
for _, entry := range selected { for _, entry := range selected {
grouped[entry.socksConn] = append(grouped[entry.socksConn], entry.item) grouped[entry.socksConn] = append(grouped[entry.socksConn], entry.item)
} }
for socksConn, items := range grouped { for socksConn, items := range grouped {
socksConn.MarkInFlight(items) socksConn.MarkInFlight(items)
} }
@@ -221,9 +223,11 @@ func (c *Client) reclaimExpiredInFlight() {
"<yellow>socks_id=<cyan>%d</cyan> reclaimed inflight requeued=<cyan>%d</cyan> dropped=<cyan>%d</cyan></yellow>", "<yellow>socks_id=<cyan>%d</cyan> reclaimed inflight requeued=<cyan>%d</cyan> dropped=<cyan>%d</cyan></yellow>",
socksConn.ID, requeued, dropped, socksConn.ID, requeued, dropped,
) )
if requeued > 0 { if requeued > 0 {
c.signalSendWork() c.signalSendWork()
} }
if dropped > 0 { if dropped > 0 {
socksConn.ConnectFailure = "max retry exceeded" socksConn.ConnectFailure = "max retry exceeded"
socksConn.CompleteConnect(fmt.Errorf("max retry exceeded")) socksConn.CompleteConnect(fmt.Errorf("max retry exceeded"))
@@ -242,6 +246,9 @@ func (w *sendWorker) postBatch(ctx context.Context, c *Client, batch protocol.Ba
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-Relay-Version", fmt.Sprintf("%d", protocol.CurrentVersion)) req.Header.Set("X-Relay-Version", fmt.Sprintf("%d", protocol.CurrentVersion))
if c.headerBuilder != nil {
c.headerBuilder.Apply(req)
}
resp, err := w.httpClient.Do(req) resp, err := w.httpClient.Do(req)
if err != nil { if err != nil {
@@ -266,6 +273,7 @@ func (w *sendWorker) postBatch(ctx context.Context, c *Client, batch protocol.Ba
if err != nil { if err != nil {
return err return err
} }
if len(respBody) == 0 { if len(respBody) == 0 {
return nil return nil
} }
@@ -274,13 +282,16 @@ func (w *sendWorker) postBatch(ctx context.Context, c *Client, batch protocol.Ba
if err != nil { if err != nil {
return err return err
} }
c.log.Debugf( c.log.Debugf(
"<gray>worker=<cyan>%d</cyan> received response batch=<cyan>%s</cyan> packets=<cyan>%d</cyan> bytes=<cyan>%d</cyan></gray>", "<gray>worker=<cyan>%d</cyan> received response batch=<cyan>%s</cyan> packets=<cyan>%d</cyan> bytes=<cyan>%d</cyan></gray>",
w.id, responseBatch.BatchID, len(responseBatch.Packets), len(respBody), w.id, responseBatch.BatchID, len(responseBatch.Packets), len(respBody),
) )
if err := c.applyResponseBatch(responseBatch); err != nil { if err := c.applyResponseBatch(responseBatch); err != nil {
return err return err
} }
return nil return nil
} }
@@ -290,6 +301,7 @@ func (c *Client) applyResponseBatch(batch protocol.Batch) error {
"<gray>apply response packet=<cyan>%s</cyan> socks_id=<cyan>%d</cyan> seq=<cyan>%d</cyan> payload_bytes=<cyan>%d</cyan> final=<cyan>%t</cyan></gray>", "<gray>apply response packet=<cyan>%s</cyan> socks_id=<cyan>%d</cyan> seq=<cyan>%d</cyan> payload_bytes=<cyan>%d</cyan> final=<cyan>%t</cyan></gray>",
packet.Type, packet.SOCKSID, packet.Sequence, len(packet.Payload), packet.Final, packet.Type, packet.SOCKSID, packet.Sequence, len(packet.Payload), packet.Final,
) )
if err := c.applyResponsePacket(packet); err != nil { if err := c.applyResponsePacket(packet); err != nil {
return err return err
} }
@@ -359,6 +371,7 @@ func (c *Client) applyResponsePacket(packet protocol.Packet) error {
"<gray>writing to local socket socks_id=<cyan>%d</cyan> bytes=<cyan>%d</cyan></gray>", "<gray>writing to local socket socks_id=<cyan>%d</cyan> bytes=<cyan>%d</cyan></gray>",
socksConn.ID, len(packet.Payload), socksConn.ID, len(packet.Payload),
) )
return socksConn.WriteToLocal(packet.Payload) return socksConn.WriteToLocal(packet.Payload)
case protocol.PacketTypeSOCKSCloseRead: case protocol.PacketTypeSOCKSCloseRead:
@@ -368,12 +381,15 @@ func (c *Client) applyResponsePacket(packet protocol.Packet) error {
"<gray>close_read applied socks_id=<cyan>%d</cyan></gray>", "<gray>close_read applied socks_id=<cyan>%d</cyan></gray>",
socksConn.ID, socksConn.ID,
) )
if err := socksConn.CloseLocalWrite(); err != nil { if err := socksConn.CloseLocalWrite(); err != nil {
return err return err
} }
if socksConn.BothLocalSidesClosed() { if socksConn.BothLocalSidesClosed() {
return socksConn.CloseLocal() return socksConn.CloseLocal()
} }
return nil return nil
case protocol.PacketTypeSOCKSCloseWrite: case protocol.PacketTypeSOCKSCloseWrite:
+75
View File
@@ -18,6 +18,14 @@ import (
type Config struct { type Config struct {
AESEncryptionKey string AESEncryptionKey string
RelayURL string RelayURL string
HTTPUserAgentsFile string
HTTPHeaderProfile string
HTTPRandomizeHeaders bool
HTTPPaddingHeader string
HTTPPaddingMinBytes int
HTTPPaddingMaxBytes int
HTTPReferer string
HTTPAcceptLanguage string
ServerHost string ServerHost string
ServerPort int ServerPort int
SOCKSHost string SOCKSHost string
@@ -46,6 +54,12 @@ func Load(path string) (Config, error) {
cfg := Config{ cfg := Config{
SOCKSHost: "127.0.0.1", SOCKSHost: "127.0.0.1",
SOCKSPort: 1080, SOCKSPort: 1080,
HTTPUserAgentsFile: "user-agents.txt",
HTTPHeaderProfile: "browser",
HTTPRandomizeHeaders: true,
HTTPPaddingHeader: "X-Padding",
HTTPPaddingMinBytes: 16,
HTTPPaddingMaxBytes: 48,
ServerHost: "127.0.0.1", ServerHost: "127.0.0.1",
ServerPort: 28080, ServerPort: 28080,
LogLevel: "INFO", LogLevel: "INFO",
@@ -91,6 +105,34 @@ func Load(path string) (Config, error) {
cfg.AESEncryptionKey = trimString(value) cfg.AESEncryptionKey = trimString(value)
case "RELAY_URL": case "RELAY_URL":
cfg.RelayURL = trimString(value) cfg.RelayURL = trimString(value)
case "HTTP_USER_AGENTS_FILE":
cfg.HTTPUserAgentsFile = trimString(value)
case "HTTP_HEADER_PROFILE":
cfg.HTTPHeaderProfile = trimString(value)
case "HTTP_RANDOMIZE_HEADERS":
randomize, err := strconv.ParseBool(value)
if err != nil {
return Config{}, fmt.Errorf("parse HTTP_RANDOMIZE_HEADERS: %w", err)
}
cfg.HTTPRandomizeHeaders = randomize
case "HTTP_PADDING_HEADER":
cfg.HTTPPaddingHeader = trimString(value)
case "HTTP_PADDING_MIN_BYTES":
size, err := strconv.Atoi(value)
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 "SERVER_HOST": case "SERVER_HOST":
cfg.ServerHost = trimString(value) cfg.ServerHost = trimString(value)
case "SERVER_PORT": case "SERVER_PORT":
@@ -217,33 +259,55 @@ func (c Config) ValidateClient() error {
if err := c.validateShared(); err != nil { if err := c.validateShared(); err != nil {
return err return err
} }
if c.SOCKSAuth && (c.SOCKSUsername == "" || c.SOCKSPassword == "") { if c.SOCKSAuth && (c.SOCKSUsername == "" || c.SOCKSPassword == "") {
return fmt.Errorf("SOCKS auth enabled but username/password missing") return fmt.Errorf("SOCKS auth enabled but username/password missing")
} }
if c.SOCKSPort < 1 || c.SOCKSPort > 65535 { if c.SOCKSPort < 1 || c.SOCKSPort > 65535 {
return fmt.Errorf("invalid SOCKS_PORT: %d", c.SOCKSPort) return fmt.Errorf("invalid SOCKS_PORT: %d", c.SOCKSPort)
} }
if strings.TrimSpace(c.RelayURL) == "" { if strings.TrimSpace(c.RelayURL) == "" {
return fmt.Errorf("RELAY_URL is required") return fmt.Errorf("RELAY_URL is required")
} }
if c.HTTPRequestTimeoutMS < 1 { if c.HTTPRequestTimeoutMS < 1 {
return fmt.Errorf("invalid HTTP_REQUEST_TIMEOUT_MS: %d", c.HTTPRequestTimeoutMS) return fmt.Errorf("invalid HTTP_REQUEST_TIMEOUT_MS: %d", c.HTTPRequestTimeoutMS)
} }
if c.WorkerPollIntervalMS < 1 { if c.WorkerPollIntervalMS < 1 {
return fmt.Errorf("invalid WORKER_POLL_INTERVAL_MS: %d", c.WorkerPollIntervalMS) return fmt.Errorf("invalid WORKER_POLL_INTERVAL_MS: %d", c.WorkerPollIntervalMS)
} }
if c.IdlePollIntervalMS < c.WorkerPollIntervalMS { if c.IdlePollIntervalMS < c.WorkerPollIntervalMS {
return fmt.Errorf("IDLE_POLL_INTERVAL_MS must be >= WORKER_POLL_INTERVAL_MS") return fmt.Errorf("IDLE_POLL_INTERVAL_MS must be >= WORKER_POLL_INTERVAL_MS")
} }
if c.AckTimeoutMS < 1 { if c.AckTimeoutMS < 1 {
return fmt.Errorf("invalid ACK_TIMEOUT_MS: %d", c.AckTimeoutMS) return fmt.Errorf("invalid ACK_TIMEOUT_MS: %d", c.AckTimeoutMS)
} }
if c.MaxRetryCount < 0 { if c.MaxRetryCount < 0 {
return fmt.Errorf("invalid MAX_RETRY_COUNT: %d", c.MaxRetryCount) return fmt.Errorf("invalid MAX_RETRY_COUNT: %d", c.MaxRetryCount)
} }
if c.HTTPHeaderProfile != "browser" && c.HTTPHeaderProfile != "minimal" {
return fmt.Errorf("invalid HTTP_HEADER_PROFILE: %s", c.HTTPHeaderProfile)
}
if c.HTTPPaddingMinBytes < 0 {
return fmt.Errorf("invalid HTTP_PADDING_MIN_BYTES: %d", c.HTTPPaddingMinBytes)
}
if c.HTTPPaddingMaxBytes < c.HTTPPaddingMinBytes {
return fmt.Errorf("HTTP_PADDING_MAX_BYTES must be >= HTTP_PADDING_MIN_BYTES")
}
if c.MaxQueueBytesPerSOCKS < c.MaxChunkSize { if c.MaxQueueBytesPerSOCKS < c.MaxChunkSize {
return fmt.Errorf("MAX_QUEUE_BYTES_PER_SOCKS must be >= MAX_CHUNK_SIZE") return fmt.Errorf("MAX_QUEUE_BYTES_PER_SOCKS must be >= MAX_CHUNK_SIZE")
} }
return nil return nil
} }
@@ -251,21 +315,27 @@ func (c Config) ValidateServer() error {
if err := c.validateShared(); err != nil { if err := c.validateShared(); err != nil {
return err return err
} }
if c.ServerPort < 1 || c.ServerPort > 65535 { if c.ServerPort < 1 || c.ServerPort > 65535 {
return fmt.Errorf("invalid SERVER_PORT: %d", c.ServerPort) return fmt.Errorf("invalid SERVER_PORT: %d", c.ServerPort)
} }
if c.SessionIdleTimeoutMS < 1 { if c.SessionIdleTimeoutMS < 1 {
return fmt.Errorf("invalid SESSION_IDLE_TIMEOUT_MS: %d", c.SessionIdleTimeoutMS) return fmt.Errorf("invalid SESSION_IDLE_TIMEOUT_MS: %d", c.SessionIdleTimeoutMS)
} }
if c.SOCKSIdleTimeoutMS < 1 { if c.SOCKSIdleTimeoutMS < 1 {
return fmt.Errorf("invalid SOCKS_IDLE_TIMEOUT_MS: %d", c.SOCKSIdleTimeoutMS) return fmt.Errorf("invalid SOCKS_IDLE_TIMEOUT_MS: %d", c.SOCKSIdleTimeoutMS)
} }
if c.ReadBodyLimitBytes < c.MaxChunkSize { if c.ReadBodyLimitBytes < c.MaxChunkSize {
return fmt.Errorf("READ_BODY_LIMIT_BYTES must be >= MAX_CHUNK_SIZE") return fmt.Errorf("READ_BODY_LIMIT_BYTES must be >= MAX_CHUNK_SIZE")
} }
if c.MaxServerQueueBytes < c.MaxChunkSize { if c.MaxServerQueueBytes < c.MaxChunkSize {
return fmt.Errorf("MAX_SERVER_QUEUE_BYTES must be >= MAX_CHUNK_SIZE") return fmt.Errorf("MAX_SERVER_QUEUE_BYTES must be >= MAX_CHUNK_SIZE")
} }
return nil return nil
} }
@@ -273,18 +343,23 @@ func (c Config) validateShared() error {
if strings.TrimSpace(c.AESEncryptionKey) == "" { if strings.TrimSpace(c.AESEncryptionKey) == "" {
return fmt.Errorf("AES_ENCRYPTION_KEY is required") return fmt.Errorf("AES_ENCRYPTION_KEY is required")
} }
if c.MaxChunkSize < 1 { if c.MaxChunkSize < 1 {
return fmt.Errorf("invalid MAX_CHUNK_SIZE: %d", c.MaxChunkSize) return fmt.Errorf("invalid MAX_CHUNK_SIZE: %d", c.MaxChunkSize)
} }
if c.MaxPacketsPerBatch < 1 { if c.MaxPacketsPerBatch < 1 {
return fmt.Errorf("invalid MAX_PACKETS_PER_BATCH: %d", c.MaxPacketsPerBatch) return fmt.Errorf("invalid MAX_PACKETS_PER_BATCH: %d", c.MaxPacketsPerBatch)
} }
if c.MaxBatchBytes < c.MaxChunkSize { if c.MaxBatchBytes < c.MaxChunkSize {
return fmt.Errorf("MAX_BATCH_BYTES must be >= MAX_CHUNK_SIZE") return fmt.Errorf("MAX_BATCH_BYTES must be >= MAX_CHUNK_SIZE")
} }
if c.WorkerCount < 1 { if c.WorkerCount < 1 {
return fmt.Errorf("invalid WORKER_COUNT: %d", c.WorkerCount) return fmt.Errorf("invalid WORKER_COUNT: %d", c.WorkerCount)
} }
return nil return nil
} }
+12
View File
@@ -0,0 +1,12 @@
# One user-agent per line.
# Empty lines and lines starting with # are ignored.
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/135.0.0.0 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Mozilla/5.0 (X11; Linux x86_64; rv:137.0) Gecko/20100101 Firefox/137.0
Mozilla/5.0 (iPhone; CPU iPhone OS 18_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1
Mozilla/5.0 (Linux; Android 15; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36
Mozilla/5.0 (Linux; Android 15; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/135.0.0.0 Mobile Safari/537.36