mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-17 21:24:37 +03:00
Add randomized relay headers and configurable user-agent rotation
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user