mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-18 06:44:35 +03:00
Add anti-buffering relay headers and optional randomized query suffixes
This commit is contained in:
+11
@@ -92,6 +92,17 @@ HTTP_REFERER = ""
|
||||
# Allowed: empty string, or any valid Accept-Language header value
|
||||
HTTP_ACCEPT_LANGUAGE = ""
|
||||
|
||||
# HTTP_RANDOMIZE_QUERY_SUFFIX:
|
||||
# If true, the client appends a randomized query parameter to RELAY_URL on each
|
||||
# outbound relay request. This can produce patterns such as:
|
||||
# - ?webhe=abc123-9kf83d-72jf0a4x-zz91m3e8c2
|
||||
# - ?r=<random>
|
||||
# - ?_=<random>
|
||||
# Existing query parameters in RELAY_URL are preserved.
|
||||
# Default: false
|
||||
# Allowed: true, false
|
||||
HTTP_RANDOMIZE_QUERY_SUFFIX = false
|
||||
|
||||
# ==============================================================================
|
||||
# HTTP TIMING / BATCH SHAPE RANDOMIZATION
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
@@ -65,6 +65,26 @@ func (b *relayHeaderBuilder) Apply(req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *relayHeaderBuilder) BuildRelayURL(rawURL string) string {
|
||||
if !b.cfg.HTTPRandomizeQuerySuffix {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
query := parsed.Query()
|
||||
key, value := b.randomQuerySuffix()
|
||||
if key == "" || value == "" {
|
||||
return rawURL
|
||||
}
|
||||
query.Set(key, value)
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func (b *relayHeaderBuilder) applyBrowserProfile(req *http.Request) {
|
||||
req.Header.Set("Accept", pickRandomString(
|
||||
"*/*",
|
||||
@@ -282,3 +302,45 @@ func randomHex(byteCount int) string {
|
||||
|
||||
return hex.EncodeToString(raw)
|
||||
}
|
||||
|
||||
func (b *relayHeaderBuilder) randomQuerySuffix() (string, string) {
|
||||
patterns := []struct {
|
||||
key string
|
||||
value func() string
|
||||
}{
|
||||
{key: "webhe", value: func() string { return randomTokenPattern(6, 8, 10) }},
|
||||
{key: "r", value: func() string { return randomHex(12) }},
|
||||
{key: "_", value: func() string { return randomAlphaNumeric(18) }},
|
||||
{key: "cache_bust", value: func() string { return randomTokenPattern(8, 6, 8) }},
|
||||
{key: "v", value: func() string { return randomTokenPattern(4, 4, 6) }},
|
||||
}
|
||||
|
||||
pattern := patterns[randomIndex(len(patterns))]
|
||||
return pattern.key, pattern.value()
|
||||
}
|
||||
|
||||
func randomTokenPattern(parts ...int) string {
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(parts))
|
||||
for _, partLength := range parts {
|
||||
values = append(values, randomAlphaNumeric(partLength))
|
||||
}
|
||||
return strings.Join(values, "-")
|
||||
}
|
||||
|
||||
func randomAlphaNumeric(length int) string {
|
||||
if length <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
var builder strings.Builder
|
||||
builder.Grow(length)
|
||||
for i := 0; i < length; i++ {
|
||||
builder.WriteByte(alphabet[randomIndex(len(alphabet))])
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"masterhttprelayvpn/internal/config"
|
||||
)
|
||||
|
||||
func TestBuildRelayURLLeavesURLUntouchedWhenSuffixRandomizationDisabled(t *testing.T) {
|
||||
builder := newRelayHeaderBuilder(config.Config{
|
||||
RelayURL: "https://example.com/relay",
|
||||
HTTPRandomizeQuerySuffix: false,
|
||||
}, nil)
|
||||
|
||||
got := builder.BuildRelayURL("https://example.com/relay")
|
||||
if got != "https://example.com/relay" {
|
||||
t.Fatalf("expected relay URL to stay unchanged, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRelayURLAddsRandomQuerySuffixWhenEnabled(t *testing.T) {
|
||||
builder := newRelayHeaderBuilder(config.Config{
|
||||
RelayURL: "https://example.com/relay?existing=1",
|
||||
HTTPRandomizeQuerySuffix: true,
|
||||
}, nil)
|
||||
|
||||
got := builder.BuildRelayURL("https://example.com/relay?existing=1")
|
||||
parsed, err := url.Parse(got)
|
||||
if err != nil {
|
||||
t.Fatalf("parse randomized relay URL: %v", err)
|
||||
}
|
||||
|
||||
query := parsed.Query()
|
||||
if query.Get("existing") != "1" {
|
||||
t.Fatalf("expected existing query parameter to be preserved, got %q", query.Get("existing"))
|
||||
}
|
||||
|
||||
randomKeys := []string{"webhe", "r", "_", "cache_bust", "v"}
|
||||
found := false
|
||||
for _, key := range randomKeys {
|
||||
if value := query.Get(key); value != "" {
|
||||
found = true
|
||||
if strings.TrimSpace(value) == "" {
|
||||
t.Fatalf("expected randomized query value for key %q to be non-empty", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected one randomized query suffix key, got query %q", parsed.RawQuery)
|
||||
}
|
||||
}
|
||||
@@ -430,7 +430,12 @@ func (c *Client) reclaimExpiredReorder() {
|
||||
|
||||
func (w *sendWorker) postBatch(ctx context.Context, c *Client, batch protocol.Batch, body []byte) error {
|
||||
pingOnly := isPingOnlyBatch(batch)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.RelayURL, bytes.NewReader(body))
|
||||
relayURL := c.cfg.RelayURL
|
||||
if c.headerBuilder != nil {
|
||||
relayURL = c.headerBuilder.BuildRelayURL(relayURL)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, relayURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
if pingOnly {
|
||||
c.failPing()
|
||||
|
||||
@@ -26,6 +26,7 @@ type Config struct {
|
||||
HTTPPaddingMaxBytes int
|
||||
HTTPReferer string
|
||||
HTTPAcceptLanguage string
|
||||
HTTPRandomizeQuerySuffix bool
|
||||
HTTPTimingJitterMS int
|
||||
HTTPBatchRandomize bool
|
||||
HTTPBatchPacketsJitter int
|
||||
@@ -74,6 +75,7 @@ func Load(path string) (Config, error) {
|
||||
HTTPPaddingHeader: "X-Padding",
|
||||
HTTPPaddingMinBytes: 16,
|
||||
HTTPPaddingMaxBytes: 48,
|
||||
HTTPRandomizeQuerySuffix: false,
|
||||
HTTPTimingJitterMS: 50,
|
||||
HTTPBatchRandomize: true,
|
||||
HTTPBatchPacketsJitter: 4,
|
||||
@@ -164,6 +166,13 @@ func Load(path string) (Config, error) {
|
||||
cfg.HTTPReferer = trimString(value)
|
||||
case "HTTP_ACCEPT_LANGUAGE":
|
||||
cfg.HTTPAcceptLanguage = trimString(value)
|
||||
case "HTTP_RANDOMIZE_QUERY_SUFFIX":
|
||||
randomize, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("parse HTTP_RANDOMIZE_QUERY_SUFFIX: %w", err)
|
||||
}
|
||||
|
||||
cfg.HTTPRandomizeQuerySuffix = randomize
|
||||
case "HTTP_TIMING_JITTER_MS":
|
||||
valueInt, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -107,6 +107,8 @@ func (s *Server) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *Server) handleRelay(w http.ResponseWriter, r *http.Request) {
|
||||
applyRelayResponseHeaders(w.Header())
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -167,6 +169,13 @@ func (s *Server) handleRelay(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(encrypted)
|
||||
}
|
||||
|
||||
func applyRelayResponseHeaders(header http.Header) {
|
||||
header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
header.Set("Pragma", "no-cache")
|
||||
header.Set("Expires", "0")
|
||||
header.Set("X-Accel-Buffering", "no")
|
||||
}
|
||||
|
||||
func (s *Server) processBatch(batch protocol.Batch) (protocol.Batch, error) {
|
||||
session := s.getOrCreateSession(batch.ClientSessionKey)
|
||||
now := time.Now()
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -319,6 +320,27 @@ func TestSOCKSStateCloseUpstreamClearsConnectionSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRelayAppliesAntiBufferingHeaders(t *testing.T) {
|
||||
srv := New(config.Config{}, nil)
|
||||
request := httptest.NewRequest("GET", "/relay", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
srv.handleRelay(recorder, request)
|
||||
|
||||
if got := recorder.Header().Get("X-Accel-Buffering"); got != "no" {
|
||||
t.Fatalf("expected X-Accel-Buffering=no, got %q", got)
|
||||
}
|
||||
if got := recorder.Header().Get("Cache-Control"); got != "no-store, no-cache, must-revalidate" {
|
||||
t.Fatalf("unexpected Cache-Control header: %q", got)
|
||||
}
|
||||
if got := recorder.Header().Get("Pragma"); got != "no-cache" {
|
||||
t.Fatalf("unexpected Pragma header: %q", got)
|
||||
}
|
||||
if got := recorder.Header().Get("Expires"); got != "0" {
|
||||
t.Fatalf("unexpected Expires header: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func testDataPacket(clientSessionKey string, socksID uint64, sequence uint64, payload string) protocol.Packet {
|
||||
packet := protocol.NewPacket(clientSessionKey, protocol.PacketTypeSOCKSData)
|
||||
packet.SOCKSID = socksID
|
||||
|
||||
Reference in New Issue
Block a user