نقل 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):
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-down | POST متدفق | 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:
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"
}تدفق الاتصال
بنية الاتصال
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) تنسق الاتصال:
- بناء URL: المخطط + المضيف + المسار + الاستعلام (
dialer.go:257-276) - عميل HTTP: يُحصل عليه من
getHTTPClientالذي يدير تجميع اتصالات Xmux (dialer.go:278) - اختيار الوضع: تلقائي أو صريح (
dialer.go:280-289) - معرف الجلسة: UUID يُولد لكل اتصال (باستثناء وضع
stream-one) (dialer.go:291-295)
رفع الحزم (وضع packet-up)
في وضع packet-up، يتم تخزين الكتابات مؤقتًا عبر أنبوب وإرسالها كطلبات POST مرقمة (splithttp/dialer.go:396-472):
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):
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:
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):
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:
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) يوجه الطلبات حسب النوع:
- التحقق من المضيف/المسار (
hub.go:91-101) - التحقق من الحشو (
hub.go:131-138) - استخراج الجلسة/الرقم التسلسلي (
hub.go:140) - طلبات الرفع (POST مع معرف جلسة + رقم تسلسلي): الحمولة تذهب إلى
uploadQueue.Push(hub.go:207-343) - طلبات التنزيل (GET أو stream-one): يفتح استجابة متدفقة (
hub.go:345-391)
إدارة الجلسات
يتم تخزين الجلسات في sync.Map وإدارتها عبر upsertSession (splithttp/hub.go:50-88):
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 الواردة بغير ترتيب حسب الرقم التسلسلي:
type uploadQueue struct {
pushedPackets chan Packet
heap uploadHeap // min-heap by Seq
nextSeq uint64
}طريقة Read (upload_queue.go:85-143) تسلم البيانات بالترتيب التسلسلي:
- انتظار الحزم على القناة
- الدفع إلى الكومة
- استخراج الحزم ذات
Seq == nextSeq - إذا كانت الحزمة التالية بغير ترتيب، انتظار المزيد
- تقييد حجم الكومة بـ
maxPacketsلمنع استنفاد الذاكرة
الاستجابة المتدفقة
لتدفقات التنزيل، يعيّن الخادم رؤوس منع التخزين المؤقت (hub.go:354-363):
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 القياسي |
header | Base64 في رؤوس مخصصة (مقسمة: X-Data-0، X-Data-1، ...) |
cookie | Base64 في ملفات تعريف الارتباط (مقسمة: 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 |
header | X-Session: uuid | X-Seq: 0 |
query | ?x_session=uuid | ?x_seq=0 |
cookie | Cookie: x_session=uuid | Cookie: x_seq=0 |
إعداد المستمع
ListenXH (splithttp/hub.go:435-533) يدعم ثلاثة أنواع من المستمعين:
- TCP (HTTP/1.1 + h2c):
http.Serverقياسي معSetUnencryptedHTTP2(true)(hub.go:516-529) - QUIC (HTTP/3):
quic.ListenEarly+http3.Server(hub.go:467-490) - مقبس نطاق 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).