Skip to content

Hysteria2

Hysteria2 هو بروتوكول وكيل مبني على QUIC مُصمّم للشبكات ذات الإنتاجية العالية والخسارة المرتفعة. يدمجه Xray-core باستخدام مكتبة apernet/quic-go (تنفيذ QUIC مُعدّل) مع تحكم مخصص بالازدحام ("Brutal") ودعم QUIC datagram لوكالة UDP.

نظرة عامة

  • الاتجاه: وارد + صادر
  • النقل: QUIC (عبر UDP)
  • التشفير: QUIC TLS 1.3 (يتولاه طبقة النقل)
  • المصادقة: كلمة مرور (يتولاها طبقة النقل)
  • وكالة TCP: عبر تدفقات QUIC
  • وكالة UDP: عبر QUIC datagrams (غير موثوقة) مع دعم التجزئة
  • التحكم بالازدحام: Brutal (عرض نطاق ثابت) أو CC QUIC القياسي

البنية المعمارية

ينقسم Hysteria2 في Xray-core بين طبقتين:

  1. طبقة النقل (transport/internet/hysteria/): تتعامل مع إنشاء اتصال QUIC وTLS والمصادقة والتحكم بالازدحام
  2. طبقة الوكيل (proxy/hysteria/): تتعامل مع بروتوكول مستوى التطبيق (تأطير طلب/استجابة TCP، صيغة رسائل UDP)
mermaid
graph TD
    subgraph "Proxy Layer (proxy/hysteria/)"
        A[Client Handler] --> B[TCP Request/Response]
        A --> C[UDP Message Framing]
        D[Server Handler] --> B
        D --> C
    end
    subgraph "Transport Layer (transport/internet/hysteria/)"
        E[QUIC Connection] --> F[Brutal CC]
        E --> G[TLS 1.3]
        E --> H[Auth Validation]
    end
    B --> E
    C --> E

صيغة السلك (Wire Format)

طلب TCP

تستخدم وكالة TCP تدفقات QUIC. كل اتصال TCP هو تدفق QUIC جديد بصيغة الطلب التالية:

+-------------------+-------------------+--------------------+-------------------+
| Address Length     | Address           | Padding Length     | Padding           |
| QUIC varint       | bytes             | QUIC varint        | bytes             |
+-------------------+-------------------+--------------------+-------------------+

المصدر: proxy/hysteria/protocol.go:28-62

الحقلالترميزالقيود
طول العنوانعدد صحيح متغير الطول QUIC1 إلى 2048
العنوانسلسلة UTF-8 (مثل "example.com:443")بصيغة host:port
طول الحشوعدد صحيح متغير الطول QUIC0 إلى 4096
الحشوبايتات عشوائيةيُتجاهل عند القراءة
go
func WriteTCPRequest(w io.Writer, addr string) error {
    padding := tcpRequestPadding.String()  // 64-512 random bytes
    // varint(addrLen) + addr + varint(paddingLen) + padding
}

المصدر: proxy/hysteria/protocol.go:64-77

استجابة TCP

+--------+-------------------+-------------------+--------------------+-------------------+
| Status | Message Length    | Message           | Padding Length     | Padding           |
| 1B     | QUIC varint      | bytes             | QUIC varint        | bytes             |
+--------+-------------------+-------------------+--------------------+-------------------+

المصدر: proxy/hysteria/protocol.go:79-142

الحقلالحجمالوصف
الحالة1 بايت0x00 = نجاح، 0x01 = خطأ
طول الرسالةعدد صحيح متغير QUIC0 إلى 2048
الرسالةبايتاترسالة خطأ (اختيارية)
طول الحشوعدد صحيح متغير QUIC0 إلى 4096
الحشوبايتاتحشو عشوائي

بعد ترويسة الاستجابة، ينقل تدفق QUIC بيانات TCP الخام.

الحشو الافتراضي

go
var (
    tcpRequestPadding  = padding.Padding{Min: 64, Max: 512}
    tcpResponsePadding = padding.Padding{Min: 128, Max: 1024}
)

المصدر: proxy/hysteria/config.go:7-9

رسالة UDP

يستخدم UDP عبر QUIC datagrams (غير موثوقة، غير مرتبة). كل datagram يحتوي على:

+------------+----------+---------+-----------+-------------------+---------+------+
| Session ID | Packet ID| Frag ID | Frag Count| Address Length    | Address | Data |
| 4B BE      | 2B BE    | 1B      | 1B        | QUIC varint       | bytes   | ...  |
+------------+----------+---------+-----------+-------------------+---------+------+

المصدر: proxy/hysteria/protocol.go:144-216

go
type UDPMessage struct {
    SessionID uint32  // 4 bytes, big-endian
    PacketID  uint16  // 2 bytes, big-endian
    FragID    uint8   // Fragment index (0-based)
    FragCount uint8   // Total fragments (1 = no fragmentation)
    Addr      string  // "host:port" with varint length prefix
    Data      []byte  // Remaining bytes
}

المصدر: proxy/hysteria/protocol.go:153-160

الحقلالحجمالوصف
معرّف الجلسة4 بايتيُحدد جلسة UDP (حالياً مُعيّن على 0)
معرّف الحزمة2 بايتيُحدد الحزمة لإعادة تجميع التجزئة
معرّف الجزء1 بايتفهرس الجزء (من 0 إلى FragCount-1)
عدد الأجزاء1 بايتإجمالي عدد الأجزاء (1 = بدون تجزئة)
طول العنوانعدد صحيح متغير QUICطول سلسلة العنوان
العنوانبايتاتالوجهة بصيغة "host:port"
البياناتالبايتات المتبقيةحمولة UDP الفعلية

ترميز الأعداد الصحيحة متغيرة الطول في QUIC

يستخدم Hysteria ترميز الأعداد الصحيحة متغيرة الطول الخاص بـ QUIC (RFC 9000):

النطاقبتات البادئةالبايتات
0-63001
64-16383012
16384-1073741823104
1073741824-4611686018427387903118
go
func varintPut(b []byte, i uint64) int {
    if i <= 63         { b[0] = uint8(i); return 1 }
    if i <= 16383      { b[0] = uint8(i>>8) | 0x40; b[1] = uint8(i); return 2 }
    if i <= 1073741823 { b[0] = uint8(i>>24) | 0x80; /* ... */ return 4 }
    // ...8-byte encoding
}

المصدر: proxy/hysteria/protocol.go:220-249

تجزئة UDP

عندما تتجاوز رسالة UDP حجم MTU لـ QUIC datagram، تُقسّم إلى أجزاء:

go
func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage {
    if m.Size() <= maxSize {
        return []UDPMessage{*m}
    }
    maxPayloadSize := maxSize - m.HeaderSize()
    fragCount := (len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize
    // Split data across fragments, sharing same PacketID
}

المصدر: proxy/hysteria/frag.go:3-27

مُعيد التجميع (Defragger)

يُعيد Defragger تجميع رسائل UDP المُجزّأة. يتعامل مع معرّف حزمة واحد في المرة — إذا وصلت حزمة جديدة قبل اكتمال جميع أجزاء الحزمة السابقة، تُتخلّى عن الحالة السابقة:

go
type Defragger struct {
    pktID uint16
    frags []*UDPMessage
    count uint8
    size  int
}

func (d *Defragger) Feed(m *UDPMessage) *UDPMessage {
    if m.FragCount <= 1 { return m }  // No fragmentation
    if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {
        // New packet, reset state
        d.pktID = m.PacketID
        d.frags = make([]*UDPMessage, m.FragCount)
    }
    // Collect fragment, assemble when complete
}

المصدر: proxy/hysteria/frag.go:29-73

المعالج الصادر (العميل)

الملف: proxy/hysteria/client.go

تدفق TCP

go
func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
    // For TCP: open QUIC stream
    conn, err := dialer.Dial(hyCtx.ContextWithRequireDatagram(ctx, isUDP), c.server.Destination)

    if target.Network == net.Network_TCP {
        // Write TCP request header
        WriteTCPRequest(bufferedWriter, target.NetAddr())
        // Read TCP response header
        ok, msg, err := ReadTCPResponse(conn)
        // Bidirectional copy
    }
}

المصدر: proxy/hysteria/client.go:49-117

تدفق UDP

لـ UDP، يستخدم العميل QUIC datagrams. مطلوب نوع InterUdpConn (من نقل hysteria):

go
if target.Network == net.Network_UDP {
    iConn := stat.TryUnwrapStatsConn(conn)
    _, ok := iConn.(*hysteria.InterUdpConn)
    if !ok {
        return errors.New("udp requires hysteria udp transport")
    }

    writer := &UDPWriter{Writer: conn, buf: make([]byte, MaxUDPSize), addr: target.NetAddr()}
    reader := &UDPReader{Reader: conn, buf: make([]byte, MaxUDPSize), df: &Defragger{}}
}

المصدر: proxy/hysteria/client.go:119-164

UDPWriter يتعامل مع التجزئة تلقائياً:

go
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
    msg := &UDPMessage{SessionID: 0, FragCount: 1, Addr: addr, Data: b.Bytes()}
    err := w.sendMsg(msg)
    var errTooLarge *quic.DatagramTooLargeError
    if go_errors.As(err, &errTooLarge) {
        msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
        fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
        for _, fMsg := range fMsgs {
            w.sendMsg(&fMsg)
        }
    }
}

المصدر: proxy/hysteria/client.go:190-235

المعالج الوارد (الخادم)

الملف: proxy/hysteria/server.go

يتعامل الخادم مع كلاً من اتصالات تدفق QUIC (TCP) واتصالات QUIC datagram (UDP):

go
func (s *Server) Process(ctx context.Context, network net.Network,
    conn stat.Connection, dispatcher routing.Dispatcher) error {

    iConn := stat.TryUnwrapStatsConn(conn)
    if _, ok := iConn.(*hysteria.InterUdpConn); ok {
        // UDP mode: read datagrams, defragger, dispatch
        for {
            msg, _ := ParseUDPMessage(b[:n])
            dfMsg := df.Feed(msg)
            if dfMsg != nil {
                dest, _ := net.ParseDestination("udp:" + dfMsg.Addr)
                // Dispatch via DispatchLink
            }
        }
    } else {
        // TCP mode: read request, dispatch, write response
        addr, _ := ReadTCPRequest(conn)
        dest, _ := net.ParseDestination("tcp:" + addr)
        WriteTCPResponse(bufferedWriter, true, "")
        dispatcher.DispatchLink(ctx, dest, &transport.Link{...})
    }
}

المصدر: proxy/hysteria/server.go:81-192

مصادقة المستخدم

يتم التحقق من المستخدمين في طبقة النقل. يستخرج الخادم معلومات المستخدم من الاتصال:

go
type User interface{ User() *protocol.MemoryUser }
if v, ok := conn.(User); ok {
    inbound.User = v.User()
}

المصدر: proxy/hysteria/server.go:88-95

يدعم account.Validator إدارة المستخدمين أثناء التشغيل (إضافة/إزالة/استعلام).

المصدر: proxy/hysteria/server.go:57-75

الثوابت

go
const (
    MaxAddressLength = 2048
    MaxMessageLength = 2048
    MaxPaddingLength = 4096
    MaxUDPSize       = 4096
)

المصدر: proxy/hysteria/protocol.go:13-17

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

  1. الاعتماد على QUIC: يعتمد Hysteria2 على github.com/apernet/quic-go، وهو نسخة معدّلة من quic-go مع تعديلات للتحكم بالازدحام Brutal وميزات أخرى خاصة بـ Hysteria.

  2. فصل النقل والبروتوكول: بخلاف معظم بروتوكولات Xray حيث تتعامل طبقة الوكيل مع التشفير، يُفوّض Hysteria جميع العمليات التشفيرية إلى طبقة نقل QUIC/TLS. تتعامل طبقة الوكيل فقط مع تأطير الطلب/الاستجابة.

  3. نوع الشبكة كـ TCP: يُبلّغ الوارد عن شبكته كـ TCP (net.Network_TCP)، رغم أن النقل الأساسي هو UDP/QUIC. وذلك لأن تدفقات QUIC تتصرف كاتصالات TCP من منظور Xray.

المصدر: proxy/hysteria/server.go:77-79

  1. متطلب Datagram: لوكالة UDP، يُمرر العميل ContextWithRequireDatagram(ctx, true) للإشارة إلى طبقة النقل بأن دعم QUIC datagram مطلوب.

المصدر: proxy/hysteria/client.go:59

  1. إعادة تجميع بسيطة: يتتبع Defragger حزمة واحدة فقط في المرة. إذا تداخلت أجزاء من حزم مختلفة، تفشل إعادة التجميع. هذا تنازل مقصود لصالح البساطة — عملياً، لا تُعاد ترتيب QUIC datagrams عادةً ضمن اتصال واحد.

  2. العنوان كسلسلة نصية: بخلاف العناوين المُرمّزة ثنائياً في VMess/Trojan/Shadowsocks، يستخدم Hysteria سلاسل "host:port" نصية للعناوين، مع بادئات طول بأعداد صحيحة متغيرة QUIC. يُبسّط هذا التنفيذ على حساب عبء أكبر قليلاً.

  3. معرّف الجلسة: حالياً مُعيّن بشكل ثابت على 0 في العميل. الحقل موجود لتعدد الجلسات المستقبلي لكنه غير مُستخدم.

المصدر: proxy/hysteria/client.go:204

  1. الحشو: يتضمن كلاً من الطلب والاستجابة حشواً بطول عشوائي لمقاومة البصمة المرورية. حشو الطلب 64-512 بايت، وحشو الاستجابة 128-1024 بايت.

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