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
+141 -21
View File
@@ -2,7 +2,6 @@ package splithttp
import (
"net/http"
"net/url"
"strings"
"github.com/xtls/xray-core/common"
@@ -43,41 +42,27 @@ func (c *Config) GetNormalizedQuery() string {
return query
}
func (c *Config) GetRequestHeader(rawURL string) http.Header {
func (c *Config) GetRequestHeader() http.Header {
header := http.Header{}
for k, v := range c.Headers {
header.Add(k, v)
}
u, _ := url.Parse(rawURL)
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
// 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
// h3's similar QPACK feature uses the same huffman table.
u.RawQuery = "x_padding=" + strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().rand()))
header.Set("Referer", u.String())
return header
}
func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
// CORS headers for the browser dialer
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST")
writer.Header().Set("Access-Control-Allow-Methods", "*")
// writer.Header().Set("X-Version", core.Version())
writer.Header().Set("X-Padding", strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().rand())))
}
func (c *Config) GetNormalizedXPaddingBytes() RangeConfig {
if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 {
return RangeConfig{
From: 100,
To: 1000,
}
func (c *Config) GetNormalizedUplinkHTTPMethod() string {
if c.UplinkHTTPMethod == "" {
return "POST"
}
return *c.XPaddingBytes
return c.UplinkHTTPMethod
}
func (c *Config) GetNormalizedScMaxEachPostBytes() RangeConfig {
@@ -121,6 +106,134 @@ func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig {
return *c.ScStreamUpServerSecs
}
func (c *Config) GetNormalizedSessionPlacement() string {
if c.SessionPlacement == "" {
return PlacementPath
}
return c.SessionPlacement
}
func (c *Config) GetNormalizedSeqPlacement() string {
if c.SeqPlacement == "" {
return PlacementPath
}
return c.SeqPlacement
}
func (c *Config) GetNormalizedUplinkDataPlacement() string {
if c.UplinkDataPlacement == "" {
return PlacementBody
}
return c.UplinkDataPlacement
}
func (c *Config) GetNormalizedSessionKey() string {
if c.SessionKey != "" {
return c.SessionKey
}
switch c.GetNormalizedSessionPlacement() {
case PlacementHeader:
return "X-Session"
case PlacementCookie, PlacementQuery:
return "x_session"
default:
return ""
}
}
func (c *Config) GetNormalizedSeqKey() string {
if c.SeqKey != "" {
return c.SeqKey
}
switch c.GetNormalizedSeqPlacement() {
case PlacementHeader:
return "X-Seq"
case PlacementCookie, PlacementQuery:
return "x_seq"
default:
return ""
}
}
func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) {
sessionPlacement := c.GetNormalizedSessionPlacement()
seqPlacement := c.GetNormalizedSeqPlacement()
sessionKey := c.GetNormalizedSessionKey()
seqKey := c.GetNormalizedSeqKey()
if sessionId != "" {
switch sessionPlacement {
case PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, sessionId)
case PlacementQuery:
q := req.URL.Query()
q.Set(sessionKey, sessionId)
req.URL.RawQuery = q.Encode()
case PlacementHeader:
req.Header.Set(sessionKey, sessionId)
case PlacementCookie:
req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId})
}
}
if seqStr != "" {
switch seqPlacement {
case PlacementPath:
req.URL.Path = appendToPath(req.URL.Path, seqStr)
case PlacementQuery:
q := req.URL.Query()
q.Set(seqKey, seqStr)
req.URL.RawQuery = q.Encode()
case PlacementHeader:
req.Header.Set(seqKey, seqStr)
case PlacementCookie:
req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr})
}
}
}
func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) {
sessionPlacement := c.GetNormalizedSessionPlacement()
seqPlacement := c.GetNormalizedSeqPlacement()
sessionKey := c.GetNormalizedSessionKey()
seqKey := c.GetNormalizedSeqKey()
if sessionPlacement == PlacementPath && seqPlacement == PlacementPath {
subpath := strings.Split(req.URL.Path[len(path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
if len(subpath) > 1 {
seqStr = subpath[1]
}
return sessionId, seqStr
}
switch sessionPlacement {
case PlacementQuery:
sessionId = req.URL.Query().Get(sessionKey)
case PlacementHeader:
sessionId = req.Header.Get(sessionKey)
case PlacementCookie:
if cookie, e := req.Cookie(sessionKey); e == nil {
sessionId = cookie.Value
}
}
switch seqPlacement {
case PlacementQuery:
seqStr = req.URL.Query().Get(seqKey)
case PlacementHeader:
seqStr = req.Header.Get(seqKey)
case PlacementCookie:
if cookie, e := req.Cookie(seqKey); e == nil {
seqStr = cookie.Value
}
}
return sessionId, seqStr
}
func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig {
if m.MaxConcurrency == nil {
return RangeConfig{
@@ -185,3 +298,10 @@ func init() {
func (c RangeConfig) rand() int32 {
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
}
func appendToPath(path, value string) string {
if strings.HasSuffix(path, "/") {
return path + value
}
return path + "/" + value
}