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 بين طبقتين:
- طبقة النقل (
transport/internet/hysteria/): تتعامل مع إنشاء اتصال QUIC وTLS والمصادقة والتحكم بالازدحام - طبقة الوكيل (
proxy/hysteria/): تتعامل مع بروتوكول مستوى التطبيق (تأطير طلب/استجابة TCP، صيغة رسائل UDP)
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
| الحقل | الترميز | القيود |
|---|---|---|
| طول العنوان | عدد صحيح متغير الطول QUIC | 1 إلى 2048 |
| العنوان | سلسلة UTF-8 (مثل "example.com:443") | بصيغة host:port |
| طول الحشو | عدد صحيح متغير الطول QUIC | 0 إلى 4096 |
| الحشو | بايتات عشوائية | يُتجاهل عند القراءة |
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 = خطأ |
| طول الرسالة | عدد صحيح متغير QUIC | 0 إلى 2048 |
| الرسالة | بايتات | رسالة خطأ (اختيارية) |
| طول الحشو | عدد صحيح متغير QUIC | 0 إلى 4096 |
| الحشو | بايتات | حشو عشوائي |
بعد ترويسة الاستجابة، ينقل تدفق QUIC بيانات TCP الخام.
الحشو الافتراضي
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
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-63 | 00 | 1 |
| 64-16383 | 01 | 2 |
| 16384-1073741823 | 10 | 4 |
| 1073741824-4611686018427387903 | 11 | 8 |
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، تُقسّم إلى أجزاء:
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 المُجزّأة. يتعامل مع معرّف حزمة واحد في المرة — إذا وصلت حزمة جديدة قبل اكتمال جميع أجزاء الحزمة السابقة، تُتخلّى عن الحالة السابقة:
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
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):
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 يتعامل مع التجزئة تلقائياً:
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):
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
مصادقة المستخدم
يتم التحقق من المستخدمين في طبقة النقل. يستخرج الخادم معلومات المستخدم من الاتصال:
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
الثوابت
const (
MaxAddressLength = 2048
MaxMessageLength = 2048
MaxPaddingLength = 4096
MaxUDPSize = 4096
)المصدر: proxy/hysteria/protocol.go:13-17
ملاحظات التنفيذ
الاعتماد على QUIC: يعتمد Hysteria2 على
github.com/apernet/quic-go، وهو نسخة معدّلة من quic-go مع تعديلات للتحكم بالازدحام Brutal وميزات أخرى خاصة بـ Hysteria.فصل النقل والبروتوكول: بخلاف معظم بروتوكولات Xray حيث تتعامل طبقة الوكيل مع التشفير، يُفوّض Hysteria جميع العمليات التشفيرية إلى طبقة نقل QUIC/TLS. تتعامل طبقة الوكيل فقط مع تأطير الطلب/الاستجابة.
نوع الشبكة كـ TCP: يُبلّغ الوارد عن شبكته كـ TCP (
net.Network_TCP)، رغم أن النقل الأساسي هو UDP/QUIC. وذلك لأن تدفقات QUIC تتصرف كاتصالات TCP من منظور Xray.
المصدر: proxy/hysteria/server.go:77-79
- متطلب Datagram: لوكالة UDP، يُمرر العميل
ContextWithRequireDatagram(ctx, true)للإشارة إلى طبقة النقل بأن دعم QUIC datagram مطلوب.
المصدر: proxy/hysteria/client.go:59
إعادة تجميع بسيطة: يتتبع
Defraggerحزمة واحدة فقط في المرة. إذا تداخلت أجزاء من حزم مختلفة، تفشل إعادة التجميع. هذا تنازل مقصود لصالح البساطة — عملياً، لا تُعاد ترتيب QUIC datagrams عادةً ضمن اتصال واحد.العنوان كسلسلة نصية: بخلاف العناوين المُرمّزة ثنائياً في VMess/Trojan/Shadowsocks، يستخدم Hysteria سلاسل
"host:port"نصية للعناوين، مع بادئات طول بأعداد صحيحة متغيرة QUIC. يُبسّط هذا التنفيذ على حساب عبء أكبر قليلاً.معرّف الجلسة: حالياً مُعيّن بشكل ثابت على
0في العميل. الحقل موجود لتعدد الجلسات المستقبلي لكنه غير مُستخدم.
المصدر: proxy/hysteria/client.go:204
- الحشو: يتضمن كلاً من الطلب والاستجابة حشواً بطول عشوائي لمقاومة البصمة المرورية. حشو الطلب 64-512 بايت، وحشو الاستجابة 128-1024 بايت.