Skip to content

نقل WebSocket

مقدمة

يغلف نقل WebSocket حركة مرور البروكسي داخل إطارات WebSocket قياسية، مما يجعلها تبدو كحركة مرور WebSocket شرعية لمراقبي الشبكة والأجهزة الوسيطة. يستخدم مكتبة gorilla/websocket لكل من العميل والخادم، ويدعم TLS مع بصمة uTLS، والبيانات المبكرة (0-RTT عبر رأس Sec-WebSocket-Protocol)، ومتصل متصفح اختياري للتشغيل في سياقات المتصفح.

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

مسجل باسم "websocket" (transport/internet/websocket/ws.go:8):

go
const protocolName = "websocket"
  • المتصل: websocket/dialer.go:42-44
  • المستمع: websocket/hub.go:174-176
  • الإعدادات: websocket/config.go:33-37

تدفق الاتصال

نقطة الدخول

websocket.Dial (websocket/dialer.go:21-40):

go
func Dial(ctx context.Context, dest net.Destination,
    streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
    if streamSettings.ProtocolSettings.(*Config).Ed > 0 {
        // Early data mode: defer actual dial until first Write
        conn = &delayDialConn{...}
    } else {
        conn, err = dialWebSocket(ctx, dest, streamSettings, nil)
    }
    return stat.Connection(conn), nil
}

تفاصيل اتصال WebSocket

dialWebSocket (websocket/dialer.go:46-129) يبني websocket.Dialer وينفذ الترقية:

go
func dialWebSocket(ctx context.Context, dest net.Destination,
    streamSettings *internet.MemoryStreamConfig, ed []byte) (net.Conn, error) {
    wsSettings := streamSettings.ProtocolSettings.(*Config)

    dialer := &websocket.Dialer{
        NetDial: func(network, addr string) (net.Conn, error) {
            return internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
        },
        ReadBufferSize:   4 * 1024,
        WriteBufferSize:  4 * 1024,
        HandshakeTimeout: time.Second * 8,
    }
    // ...
}

الجوانب الرئيسية:

  1. اتصال النظام: رد نداء NetDial يفوض إلى internet.DialSystem، مع تطبيق خيارات المقبس.
  2. TLS: عند تكوين TLS، يصبح protocol هو "wss" ويتم تعيين dialer.TLSClientConfig.
  3. بصمة uTLS: عند تعيين بصمة، يتم تجاوز dialer.NetDialTLSContext لاستخدام tls.UClient مع WebsocketHandshakeContext (يفرض ALPN http/1.1) (websocket/dialer.go:66-87).
  4. بناء URI: ws:// أو wss:// مع المضيف والمسار المُطبّع (websocket/dialer.go:90-94).
  5. متصل المتصفح: إذا كان متاحًا، يتجاوز الاتصال العادي بالكامل (websocket/dialer.go:96-103).
  6. رأس Host: الأولوية: Host من الإعداد > ServerName من TLS > عنوان الوجهة (websocket/dialer.go:106-113).

آلية البيانات المبكرة

عند تكوين Ed > 0، يؤدي أول استدعاء Write إلى تشغيل اتصال WebSocket الفعلي. يتم إرسال البايتات الأولية كبيانات مشفرة بـ base64 في رأس Sec-WebSocket-Protocol:

go
// websocket/dialer.go:114-117
if ed != nil {
    header.Set("Sec-WebSocket-Protocol", base64.RawURLEncoding.EncodeToString(ed))
}

بنية delayDialConn (websocket/dialer.go:131-184) تنفذ هذا الاتصال المؤجل:

mermaid
sequenceDiagram
    participant App as Application
    participant DDC as delayDialConn
    participant WS as WebSocket Server

    App->>DDC: Write(data)
    Note over DDC: First write triggers dial
    alt len(data) <= Ed
        DDC->>WS: WebSocket Upgrade + data in Sec-WebSocket-Protocol
        DDC-->>App: len(data), nil
    else len(data) > Ed
        DDC->>WS: WebSocket Upgrade (no early data)
        DDC->>WS: Write(data) via WebSocket frame
    end
    App->>DDC: Read(buf)
    Note over DDC: Blocks until dialed channel signals
    DDC->>WS: Read from WebSocket

يستخرج الخادم البيانات المبكرة من رأس Sec-WebSocket-Protocol (websocket/hub.go:55-59):

go
if str := request.Header.Get("Sec-WebSocket-Protocol"); str != "" {
    if ed, err := base64.RawURLEncoding.DecodeString(replacer.Replace(str)); err == nil && len(ed) > 0 {
        extraReader = bytes.NewReader(ed)
        responseHeader.Set("Sec-WebSocket-Protocol", str)
    }
}

تدفق الاستماع

إعداد خادم HTTP

ListenWS (websocket/hub.go:98-162) يعد خادم HTTP للتعامل مع ترقيات WebSocket:

go
func ListenWS(ctx context.Context, address net.Address, port net.Port,
    streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
    // Create net.Listener (TCP or Unix)
    // Optionally wrap with TLS
    l.server = http.Server{
        Handler: &requestHandler{
            host: wsSettings.Host,
            path: wsSettings.GetNormalizedPath(),
            ln:   l,
        },
        ReadHeaderTimeout: time.Second * 4,
        MaxHeaderBytes:    8192,
    }
    go l.server.Serve(l.listener)
    return l, err
}

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

requestHandler.ServeHTTP (websocket/hub.go:41-88) يتحقق من الاتصالات ويقوم بترقيتها:

  1. التحقق من المضيف: إذا تم تكوينه، يتحقق من request.Host مقابل config.Host (hub.go:42-46)
  2. التحقق من المسار: مطابقة تامة لـ request.URL.Path مع المسار المُعدّ (hub.go:47-51)
  3. استخراج البيانات المبكرة: فك تشفير رأس Sec-WebSocket-Protocol (hub.go:55-59)
  4. ترقية WebSocket: يستخدم upgrader.Upgrade من gorilla مع CheckOrigin متساهل (hub.go:32-39، hub.go:62)
  5. X-Forwarded-For: يستخرج عنوان IP الحقيقي للعميل من رؤوس إعادة التوجيه، مع احترام إعداد TrustedXForwardedFor (hub.go:68-85)

المُرقّي العام (websocket/hub.go:32-39):

go
var upgrader = &websocket.Upgrader{
    ReadBufferSize:   0,
    WriteBufferSize:  0,
    HandshakeTimeout: time.Second * 4,
    CheckOrigin: func(r *http.Request) bool { return true },
}

مغلف الاتصال

بنية connection (websocket/connection.go:19-22) تغلف *websocket.Conn:

go
type connection struct {
    conn       *websocket.Conn
    reader     io.Reader
    remoteAddr net.Addr
}

تنفيذ القراءة

القراءة موجهة بالرسائل (websocket/connection.go:45-59): يتم قراءة كل رسالة WebSocket بالكامل، وعند استنفاد رسالة واحدة، يتم جلب التالية عبر conn.NextReader():

go
func (c *connection) Read(b []byte) (int, error) {
    for {
        reader, err := c.getReader()
        // ...
        nBytes, err := reader.Read(b)
        if errors.Cause(err) == io.EOF {
            c.reader = nil  // message exhausted, get next
            continue
        }
        return nBytes, err
    }
}

يتم استهلاك القارئ الإضافي (البيانات المبكرة) أولاً إذا كان موجودًا.

تنفيذ الكتابة

الكتابة تنتج رسائل WebSocket ثنائية (websocket/connection.go:75-80):

go
func (c *connection) Write(b []byte) (int, error) {
    if err := c.conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
        return 0, err
    }
    return len(b), nil
}

نبضات القلب (Ping)

عند تكوين HeartbeatPeriod، يرسل روتين متزامن إطارات تحكم WebSocket Ping (websocket/connection.go:26-35):

go
func NewConnection(conn *websocket.Conn, remoteAddr net.Addr,
    extraReader io.Reader, heartbeatPeriod uint32) *connection {
    if heartbeatPeriod != 0 {
        go func() {
            for {
                time.Sleep(time.Duration(heartbeatPeriod) * time.Second)
                if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Time{}); err != nil {
                    break
                }
            }
        }()
    }
    // ...
}

الإغلاق الرشيق

عند الإغلاق، يتم إرسال رسالة WebSocket CloseMessage قبل إغلاق الاتصال الأساسي (websocket/connection.go:89-101):

go
func (c *connection) Close() error {
    c.conn.WriteControl(websocket.CloseMessage,
        websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
        time.Now().Add(time.Second*5))
    c.conn.Close()
    // ...
}

تنسيق السلك

يستخدم نقل WebSocket تأطير RFC 6455 القياسي:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <hash>
Sec-WebSocket-Protocol: <base64 early data>  (if Ed > 0)

[Binary WebSocket frames containing proxy data]

كل استدعاء Write ينتج رسالة WebSocket ثنائية واحدة. كل استدعاء Read يقرأ من الرسالة الحالية حتى EOF، ثم ينتقل إلى التالية.

خيارات الإعدادات

من websocket/config.go:

  • Path: مسار WebSocket، مُطبّع بـ / في البداية (config.go:11-20)
  • Host: قيمة رأس Host المتوقعة للتحقق من جانب الخادم
  • Header: رؤوس HTTP إضافية (قاموس)، القيمة الافتراضية لـ User-Agent هي وكيل Chrome (config.go:22-31)
  • Ed: الحد الأقصى لحجم البيانات المبكرة بالبايت. اضبطه على 0 للتعطيل.
  • HeartbeatPeriod: الفاصل الزمني بالثواني لإطارات WebSocket Ping. 0 للتعطيل.
  • AcceptProxyProtocol: تفعيل بروتوكول PROXY على المستمع.

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

  • gorilla/websocket: يُستخدم لكل من العميل والخادم. مُرقّي الخادم لديه مخازن مؤقتة بحجم صفر (ReadBufferSize: 0, WriteBufferSize: 0) لتقليل استخدام الذاكرة، بينما يستخدم متصل العميل مخازن مؤقتة بحجم 4 كيلوبايت.
  • uTLS و WebSocket: عند استخدام بصمة uTLS، يفرض WebsocketHandshakeContext استخدام http/1.1 في امتداد ALPN، لأن WebSocket يتطلب HTTP/1.1.
  • متصل المتصفح: عندما تعيد browser_dialer.HasBrowserDialer() القيمة true، يتم إنشاء اتصالات WebSocket عبر واجهة WebSocket API الخاصة بالمتصفح بدلاً من مكدس شبكة Go. يُستخدم هذا لسيناريوهات النشر المعتمدة على المتصفح.
  • ترميز Base64: البيانات المبكرة تستخدم RawURLEncoding (بدون حشو، أحرف آمنة لعناوين URL) للتوافق مع كل من V2Ray/V2Fly و Xray.
  • مُبدّل النصوص: جانب الخادم يستخدم strings.NewReplacer("+", "-", "/", "_", "=", "") للتطبيع بين متغيرات base64 القياسية والآمنة لعناوين URL (hub.go:30).
  • العنوان البعيد: مغلف connection يتجاوز RemoteAddr() لدعم X-Forwarded-For، لذا قد يختلف العنوان المُبلغ عنه عن نظير TCP الفعلي.

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