XHTTP transport: New options for bypassing CDN's detection (#5414)

Usage: https://github.com/XTLS/Xray-core/pull/5414#issuecomment-3770071786

Closes https://github.com/XTLS/Xray-core/issues/4346

---------

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
This commit is contained in:
Dmitrii Makhno
2026-01-31 16:34:13 +03:00
committed by GitHub
parent 61e1153157
commit 5b849d51a9
13 changed files with 1073 additions and 208 deletions

View File

@@ -4,9 +4,10 @@ import (
"bytes"
"context"
gotls "crypto/tls"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
@@ -100,6 +101,24 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
}
h.config.WriteResponseHeader(writer)
length := int(h.config.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}
if h.config.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: h.config.XPaddingPlacement,
Key: h.config.XPaddingKey,
Header: h.config.XPaddingHeader,
}
config.Method = PaddingMethod(h.config.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementHeader,
Header: "X-Padding",
}
}
h.config.ApplyXPaddingToHeader(writer.Header(), config)
/*
clientVer := []int{0, 0, 0}
@@ -110,29 +129,15 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
*/
validRange := h.config.GetNormalizedXPaddingBytes()
paddingLength := 0
paddingValue, paddingPlacement := h.config.ExtractXPaddingFromRequest(request, h.config.XPaddingObfsMode)
referrer := request.Header.Get("Referer")
if referrer != "" {
if referrerURL, err := url.Parse(referrer); err == nil {
// Browser dialer cannot control the host part of referrer header, so only check the query
paddingLength = len(referrerURL.Query().Get("x_padding"))
}
} else {
paddingLength = len(request.URL.Query().Get("x_padding"))
}
if int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To {
errors.LogInfo(context.Background(), "invalid x_padding length:", int32(paddingLength))
if !h.config.IsPaddingValid(paddingValue, validRange.From, validRange.To, PaddingMethod(h.config.XPaddingMethod)) {
errors.LogInfo(context.Background(), "invalid padding ("+paddingPlacement+") length:", int32(len(paddingValue)))
writer.WriteHeader(http.StatusBadRequest)
return
}
sessionId := ""
subpath := strings.Split(request.URL.Path[len(h.path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
sessionId, seqStr := h.config.ExtractMetaFromRequest(request, h.path)
if sessionId == "" && h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-one" && h.config.Mode != "stream-up" {
errors.LogInfo(context.Background(), "stream-one mode is not allowed")
@@ -178,14 +183,29 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
currentSession = h.upsertSession(sessionId)
}
scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To)
uplinkHTTPMethod := h.config.GetNormalizedUplinkHTTPMethod()
isUplinkRequest := false
if request.Method == "POST" && sessionId != "" { // stream-up, packet-up
seq := ""
if len(subpath) > 1 {
seq = subpath[1]
if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
isUplinkRequest = true
}
uplinkDataPlacement := h.config.GetNormalizedUplinkDataPlacement()
uplinkDataKey := h.config.UplinkDataKey
switch uplinkDataPlacement {
case PlacementHeader:
if request.Header.Get(uplinkDataKey+"-Upstream") == "1" {
isUplinkRequest = true
}
case PlacementCookie:
if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" {
isUplinkRequest = true
}
}
if seq == "" {
if isUplinkRequest && sessionId != "" { // stream-up, packet-up
if seqStr == "" {
if h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-up" {
errors.LogInfo(context.Background(), "stream-up mode is not allowed")
writer.WriteHeader(http.StatusBadRequest)
@@ -207,6 +227,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
writer.Header().Set("Cache-Control", "no-store")
writer.WriteHeader(http.StatusOK)
scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs()
referrer := request.Header.Get("Referer")
if referrer != "" && scStreamUpServerSecs.To > 0 {
go func() {
for {
@@ -233,7 +254,62 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return
}
payload, err := io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
var payload []byte
if uplinkDataPlacement != PlacementBody {
var encodedStr string
switch uplinkDataPlacement {
case PlacementHeader:
dataLenStr := request.Header.Get(uplinkDataKey + "-Length")
if dataLenStr != "" {
dataLen, _ := strconv.Atoi(dataLenStr)
var chunks []string
i := 0
for {
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
chunks = append(chunks, chunk)
i++
}
encodedStr = strings.Join(chunks, "")
if len(encodedStr) != dataLen {
encodedStr = ""
}
}
case PlacementCookie:
var chunks []string
i := 0
for {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := request.Cookie(cookieName); c != nil {
chunks = append(chunks, c.Value)
i++
} else {
break
}
}
if len(chunks) > 0 {
encodedStr = strings.Join(chunks, "")
}
}
if encodedStr != "" {
payload, err = base64.RawURLEncoding.DecodeString(encodedStr)
} else {
errors.LogInfoInner(context.Background(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement)
writer.WriteHeader(http.StatusInternalServerError)
return
}
} else {
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
}
if len(payload) > scMaxEachPostBytes {
errors.LogInfo(context.Background(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.")
@@ -247,7 +323,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
return
}
seqInt, err := strconv.ParseUint(seq, 10, 64)
seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)")
writer.WriteHeader(http.StatusInternalServerError)
@@ -256,7 +332,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
err = currentSession.uploadQueue.Push(Packet{
Payload: payload,
Seq: seqInt,
Seq: seq,
})
if err != nil {