Skip to content

نقل SplitHTTP (XHTTP)

مقدمة

SplitHTTP (يُسمى داخليًا XHTTP) هو نقل متعدد الاستخدامات قائم على HTTP يقسم حركة مرور البروكسي ثنائية الاتجاه عبر طلبات HTTP منفصلة. التنزيل (من الخادم إلى العميل) يستخدم تدفق استجابة GET طويل الأمد. الرفع (من العميل إلى الخادم) يستخدم إما طلبات POST متدفقة أو طلبات POST فردية مرقمة مع إعادة التجميع. يدعم HTTP/1.1 و HTTP/2 (h2c و TLS) و HTTP/3 (QUIC)، بالإضافة إلى REALITY وتعدد إرسال الاتصالات (Xmux) وخيارات حشو/تشويش واسعة.

تسجيل البروتوكول

مسجل باسم "splithttp" (transport/internet/splithttp/splithttp.go:3):

go
const protocolName = "splithttp"
  • المتصل: splithttp/dialer.go:243-245
  • المستمع: splithttp/hub.go:564-566
  • الإعدادات: splithttp/config.go:296-300

أوضاع التشغيل

يدعم SplitHTTP أوضاعًا متعددة يتم اختيارها بواسطة حقل الإعداد Mode (splithttp/dialer.go:281-289):

الوضعالرفعالتنزيلحالة الاستخدام
stream-oneتدفق ثنائي الاتجاه واحدنفس التدفقازدواج كامل، مشابه لـ WebSocket
stream-up + stream-downPOST متدفقGET متدفقتدفقات منفصلة، متوافق مع CDN
packet-up + stream-downحزم POST مرقمةGET متدفقالأكثر توافقًا مع CDN (الافتراضي)

منطق الاختيار التلقائي:

  • الافتراضي: packet-up
  • مع REALITY: stream-one (أو stream-up إذا كان DownloadSettings موجودًا)

اختيار إصدار HTTP

decideHTTPVersion (splithttp/dialer.go:78-95) يحدد إصدار HTTP:

go
func decideHTTPVersion(tlsConfig *tls.Config, realityConfig *reality.Config) string {
    if realityConfig != nil { return "2" }
    if tlsConfig == nil { return "1.1" }
    if len(tlsConfig.NextProtocol) != 1 { return "2" }
    if tlsConfig.NextProtocol[0] == "http/1.1" { return "1.1" }
    if tlsConfig.NextProtocol[0] == "h3" { return "3" }
    return "2"
}

تدفق الاتصال

بنية الاتصال

mermaid
flowchart TD
    subgraph "Client Side"
        APP[Application Write]
        PIPE[Pipe Buffer]
        UPQ[Upload Goroutine]
        DL[Download Reader]
    end

    subgraph "HTTP Layer"
        POST1["POST /path/session/0"]
        POST2["POST /path/session/1"]
        POST3["POST /path/session/N"]
        GET["GET /path/session (SSE)"]
    end

    subgraph "Server Side"
        UQ[Upload Queue + Heap]
        SRV[Server Handler]
        RSP[Response Writer]
    end

    APP --> PIPE --> UPQ
    UPQ --> POST1 & POST2 & POST3
    POST1 & POST2 & POST3 --> UQ --> SRV
    SRV --> RSP --> GET --> DL

إعداد العميل

دالة Dial (splithttp/dialer.go:247-476) تنسق الاتصال:

  1. بناء URL: المخطط + المضيف + المسار + الاستعلام (dialer.go:257-276)
  2. عميل HTTP: يُحصل عليه من getHTTPClient الذي يدير تجميع اتصالات Xmux (dialer.go:278)
  3. اختيار الوضع: تلقائي أو صريح (dialer.go:280-289)
  4. معرف الجلسة: UUID يُولد لكل اتصال (باستثناء وضع stream-one) (dialer.go:291-295)

رفع الحزم (وضع packet-up)

في وضع packet-up، يتم تخزين الكتابات مؤقتًا عبر أنبوب وإرسالها كطلبات POST مرقمة (splithttp/dialer.go:396-472):

go
go func() {
    var seq int64
    for {
        // Read batched data from upload pipe
        chunk, err := uploadPipeReader.ReadMultiBuffer()
        // POST with sequence number
        go httpClient.PostPacket(ctx, url.String(), sessionId, seqStr, &chunk, ...)
        seq += 1
    }
}()

المعاملات الرئيسية:

  • scMaxEachPostBytes: الحد الأقصى للحمولة لكل POST (الافتراضي 1 ميجابايت، نطاق عشوائي)
  • scMinPostsIntervalMs: الحد الأدنى للتأخير بين طلبات POST (الافتراضي 30 مللي ثانية، عشوائي)

الرفع المتدفق (وضع stream-up)

في وضع stream-up، يحمل طلب POST واحد طويل الأمد جميع بيانات الرفع عبر httpClient.OpenStream (dialer.go:385-394).

تدفق التنزيل

لجميع الأوضاع باستثناء stream-one، يفتح طلب GET استجابة متدفقة (dialer.go:376-384):

go
conn.reader, conn.remoteAddr, conn.localAddr, err =
    httpClient2.OpenStream(ctx, requestURL2.String(), sessionId, nil, false)

يصبح جسم الاستجابة جانب reader من splitConn.

إعدادات تنزيل منفصلة

يسمح DownloadSettings لتدفق التنزيل باستخدام إعدادات خادم/TLS/نقل مختلفة تمامًا (dialer.go:302-339). هذا يمكّن من إعدادات حيث يمر الرفع عبر حافة CDN واحدة والتنزيل عبر أخرى.

تنفيذ عميل HTTP

DefaultDialerClient

DefaultDialerClient (splithttp/client.go:31-39) ينفذ DialerClient:

go
type DefaultDialerClient struct {
    transportConfig *Config
    client          *http.Client
    httpVersion     string
    uploadRawPool   *sync.Pool
    dialUploadConn  func(ctxInner context.Context) (net.Conn, error)
}

إنشاء نقل HTTP

createHTTPClient (splithttp/dialer.go:97-241) ينشئ http.RoundTripper المناسب:

  • HTTP/3: http3.Transport مع اتصال QUIC و keepalive قابل للتكوين (dialer.go:145-200)
  • HTTP/2: http2.Transport مع DialTLSContext مخصص (dialer.go:201-214)
  • HTTP/1.1: http.Transport قياسي مع DisableKeepAlives: true (التنزيلات المقطعة بها مشاكل مع keep-alives) (dialer.go:215-228)

تجميع اتصالات H1

لرفع POST عبر HTTP/1.1، يتم تجميع اتصالات TCP الخام عبر sync.Pool (splithttp/client.go:218-262):

go
uploadConn = c.uploadRawPool.Get()
// ... write request ...
c.uploadRawPool.Put(uploadConn)

مغلف H1Conn (splithttp/h1_conn.go) يتتبع الاستجابات غير المقروءة لدعم أنبوب HTTP/1.1.

OpenStream

OpenStream (splithttp/client.go:45-117) يستخدم httptrace.ClientTrace لالتقاط العناوين البعيدة/المحلية الفعلية بمجرد إنشاء اتصال TCP:

go
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
    GotConn: func(connInfo httptrace.GotConnInfo) {
        remoteAddr = connInfo.Conn.RemoteAddr()
        localAddr = connInfo.Conn.LocalAddr()
        gotConn.Close()
    },
})

تدفق الخادم

معالج الطلبات

requestHandler.ServeHTTP (splithttp/hub.go:90-396) يوجه الطلبات حسب النوع:

  1. التحقق من المضيف/المسار (hub.go:91-101)
  2. التحقق من الحشو (hub.go:131-138)
  3. استخراج الجلسة/الرقم التسلسلي (hub.go:140)
  4. طلبات الرفع (POST مع معرف جلسة + رقم تسلسلي): الحمولة تذهب إلى uploadQueue.Push (hub.go:207-343)
  5. طلبات التنزيل (GET أو stream-one): يفتح استجابة متدفقة (hub.go:345-391)

إدارة الجلسات

يتم تخزين الجلسات في sync.Map وإدارتها عبر upsertSession (splithttp/hub.go:50-88):

go
func (h *requestHandler) upsertSession(sessionId string) *httpSession {
    // Fast path: Load from sync.Map
    // Slow path: Create new with mutex
    s := &httpSession{
        uploadQueue:      NewUploadQueue(maxBufferedPosts),
        isFullyConnected: done.New(),
    }
    // Auto-reap after 30 seconds if GET never connects
    go func() {
        time.Sleep(30 * time.Second)
        shouldReap.Close()
    }()
}

قائمة انتظار الرفع (إعادة تجميع الحزم)

uploadQueue (splithttp/upload_queue.go) هي قائمة أولوية تعيد ترتيب حمولات POST الواردة بغير ترتيب حسب الرقم التسلسلي:

go
type uploadQueue struct {
    pushedPackets chan Packet
    heap          uploadHeap  // min-heap by Seq
    nextSeq       uint64
}

طريقة Read (upload_queue.go:85-143) تسلم البيانات بالترتيب التسلسلي:

  1. انتظار الحزم على القناة
  2. الدفع إلى الكومة
  3. استخراج الحزم ذات Seq == nextSeq
  4. إذا كانت الحزمة التالية بغير ترتيب، انتظار المزيد
  5. تقييد حجم الكومة بـ maxPackets لمنع استنفاد الذاكرة

الاستجابة المتدفقة

لتدفقات التنزيل، يعيّن الخادم رؤوس منع التخزين المؤقت (hub.go:354-363):

go
writer.Header().Set("X-Accel-Buffering", "no")     // nginx
writer.Header().Set("Cache-Control", "no-store")     // CDNs
writer.Header().Set("Content-Type", "text/event-stream")  // SSE hint

موضع بيانات الرفع

يمكن وضع بيانات الرفع في أجزاء مختلفة من طلب HTTP (hub.go:196-205، config.go:127-132):

الموضعالوصف
body (الافتراضي)جسم POST القياسي
headerBase64 في رؤوس مخصصة (مقسمة: X-Data-0، X-Data-1، ...)
cookieBase64 في ملفات تعريف الارتباط (مقسمة: data_0، data_1، ...)

تعدد إرسال الاتصالات (Xmux)

نظام Xmux (splithttp/mux.go) يجمع اتصالات HTTP عبر جلسات بروكسي متعددة:

  • XmuxManager: يدير مجموعة من نُسخ XmuxClient
  • XmuxClient: يغلف DialerClient (اتصال HTTP) مع تتبع الاستخدام
  • خيارات الإعداد: MaxConcurrency، MaxConnections، CMaxReuseTimes، HMaxRequestTimes، HMaxReusableSecs، HKeepAlivePeriod

عندما يتجاوز XmuxClient حد طلباته أو عمره، يتم إنشاء اتصال HTTP جديد تلقائيًا (dialer.go:448-450).

نظام الحشو

يتضمن SplitHTTP نظام حشو شامل لتشويش حركة المرور:

X-Padding

حشو عشوائي قابل للتكوين يُضاف إلى الطلبات والاستجابات (splithttp/xpadding.go):

  • XPaddingBytes: نطاق لطول الحشو (عشوائي لكل طلب)
  • خيارات الموضع: رأس، ملف تعريف ارتباط، استعلام، جسم
  • وضع التشويش: عندما يكون XPaddingObfsMode مفعلاً، يُوضع الحشو وفقًا لقواعد الإعداد مع طرق مخصصة

موضع الجلسة/الرقم التسلسلي

يمكن وضع معرفات الجلسة والأرقام التسلسلية في مواقع مختلفة (config.go:162-239):

الموضعمثال الجلسةمثال الرقم التسلسلي
path (الافتراضي)/base/uuid//base/uuid/0
headerX-Session: uuidX-Seq: 0
query?x_session=uuid?x_seq=0
cookieCookie: x_session=uuidCookie: x_seq=0

إعداد المستمع

ListenXH (splithttp/hub.go:435-533) يدعم ثلاثة أنواع من المستمعين:

  1. TCP (HTTP/1.1 + h2c): http.Server قياسي مع SetUnencryptedHTTP2(true) (hub.go:516-529)
  2. QUIC (HTTP/3): quic.ListenEarly + http3.Server (hub.go:467-490)
  3. مقبس نطاق Unix: لسلاسل البروكسي المحلية (hub.go:458-466)

يمكن لـ TLS و REALITY تغليف مستمع TCP (hub.go:503-511).

أمثلة تنسيق السلك

وضع Packet-Up

Client -> Server (upload, repeated):
POST /path/session-uuid/0 HTTP/1.1
Content-Length: 65536
X-Padding: <random>

[payload bytes, seq 0]

POST /path/session-uuid/1 HTTP/1.1
Content-Length: 32768
X-Padding: <random>

[payload bytes, seq 1]

Client <- Server (download, single long-lived):
GET /path/session-uuid HTTP/1.1

HTTP/1.1 200 OK
X-Accel-Buffering: no
Content-Type: text/event-stream
Cache-Control: no-store

[streaming response bytes...]

وضع Stream-One

POST /path/ HTTP/2
Content-Type: application/grpc

[bidirectional streaming, upload in request body, download in response body]

ملاحظات التنفيذ

  • الاسم: يُسمى داخليًا "XHTTP" في رسائل السجل، مسجل كـ "splithttp" للتوافق مع الإصدارات السابقة.
  • تعطيل keep-alive في HTTP/1.1: التنزيلات بالنقل المقطع بها مشاكل مع keep-alives وسياقات الاتصال المخصصة، لذا يعطل HTTP/1.1 keep-alives (dialer.go:225).
  • الحد الأدنى لـ scMaxEachPostBytes: يجب أن يكون أكبر من buf.Size (الافتراضي ~8 كيلوبايت) وإلا سيحدث ذعر (dialer.go:399-401).
  • تخزين أنبوب الرفع المؤقت: يتم تجميع عدة استدعاءات Write تلقائيًا في طلبات POST أكبر عبر مخزن الأنبوب المؤقت، وهو أمر حاسم لعرض النطاق الترددي (dialer.go:439-441).
  • مدة حياة الجلسة 30 ثانية: إذا لم يصل طلب GET خلال 30 ثانية من إنشاء الجلسة، يتم حصدها (hub.go:74-77).
  • متصل المتصفح: BrowserDialerClient (splithttp/browser_client.go) يستخدم واجهة fetch API الخاصة بالمتصفح للبيئات التي لا يتوفر فيها وصول مباشر للشبكة.
  • حماية تجاوز الكومة: تحد قائمة انتظار الرفع حجم كومتها بـ maxPackets. إذا تم تجاوزه، يتم تمزيق الاتصال (upload_queue.go:127-131).
  • context.WithoutCancel: طلبات HTTP تستخدم context.WithoutCancel(ctx) لمنع إلغاء الطلب من قتل اتصال HTTP الأساسي قبل الأوان (client.go:62، client.go:134).
  • FakePacketConn: عندما يحتاج QUIC إلى اتصال TCP (مثل QUIC-over-TCP)، يغلفه FakePacketConn (dialer.go:191).

تحليل تقني لأغراض إعادة التنفيذ.