mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-18 23:54:37 +03:00
Add inbound packet reordering with gap timeout and buffer limits
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// ==============================================================================
|
||||
// MasterHttpRelayVPN
|
||||
// Author: MasterkinG32
|
||||
// Github: https://github.com/masterking32
|
||||
// Year: 2026
|
||||
// ==============================================================================
|
||||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"masterhttprelayvpn/internal/protocol"
|
||||
)
|
||||
|
||||
func (s *SOCKSConnection) queueInboundPacket(packet protocol.Packet, maxBuffered int) ([]protocol.Packet, bool, bool) {
|
||||
s.reorderMu.Lock()
|
||||
defer s.reorderMu.Unlock()
|
||||
|
||||
expected := s.expectedInboundSequenceLocked()
|
||||
if packet.Sequence < expected {
|
||||
return nil, true, false
|
||||
}
|
||||
if _, exists := s.PendingInbound[packet.Sequence]; exists {
|
||||
return nil, true, false
|
||||
}
|
||||
if len(s.PendingInbound) >= maxBuffered {
|
||||
return nil, false, true
|
||||
}
|
||||
|
||||
s.PendingInbound[packet.Sequence] = PendingInboundPacket{
|
||||
Packet: packet,
|
||||
QueuedAt: time.Now(),
|
||||
}
|
||||
|
||||
if !s.ConnectAccepted {
|
||||
return nil, false, false
|
||||
}
|
||||
return s.drainReadyInboundLocked(), false, false
|
||||
}
|
||||
|
||||
func (s *SOCKSConnection) activateInboundDrain() []protocol.Packet {
|
||||
s.reorderMu.Lock()
|
||||
defer s.reorderMu.Unlock()
|
||||
return s.drainReadyInboundLocked()
|
||||
}
|
||||
|
||||
func (s *SOCKSConnection) expectedInboundSequenceLocked() uint64 {
|
||||
if s.NextInboundSequence == 0 {
|
||||
return 1
|
||||
}
|
||||
return s.NextInboundSequence
|
||||
}
|
||||
|
||||
func (s *SOCKSConnection) drainReadyInboundLocked() []protocol.Packet {
|
||||
expected := s.expectedInboundSequenceLocked()
|
||||
ready := make([]protocol.Packet, 0)
|
||||
for {
|
||||
pending, ok := s.PendingInbound[expected]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
ready = append(ready, pending.Packet)
|
||||
delete(s.PendingInbound, expected)
|
||||
expected++
|
||||
}
|
||||
s.NextInboundSequence = expected
|
||||
return ready
|
||||
}
|
||||
|
||||
func (s *SOCKSConnection) hasExpiredInboundGap(timeout time.Duration) bool {
|
||||
if timeout <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
s.reorderMu.Lock()
|
||||
defer s.reorderMu.Unlock()
|
||||
now := time.Now()
|
||||
for _, pending := range s.PendingInbound {
|
||||
if now.Sub(pending.QueuedAt) >= timeout {
|
||||
clear(s.PendingInbound)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isReorderSequencedPacket(packetType protocol.PacketType) bool {
|
||||
switch packetType {
|
||||
case protocol.PacketTypeSOCKSData,
|
||||
protocol.PacketTypeSOCKSCloseRead,
|
||||
protocol.PacketTypeSOCKSCloseWrite,
|
||||
protocol.PacketTypeSOCKSRST:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ func (w *sendWorker) run(ctx context.Context, c *Client) {
|
||||
}
|
||||
|
||||
c.reclaimExpiredInFlight()
|
||||
c.reclaimExpiredReorder()
|
||||
batch, selected := c.buildNextBatch()
|
||||
if len(batch.Packets) == 0 {
|
||||
c.waitForSendWork(ctx, c.jitterDuration(pollInterval))
|
||||
@@ -272,6 +273,22 @@ func (c *Client) reclaimExpiredInFlight() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) reclaimExpiredReorder() {
|
||||
timeout := time.Duration(c.cfg.ReorderTimeoutMS) * time.Millisecond
|
||||
for _, socksConn := range c.socksConnections.Snapshot() {
|
||||
if !socksConn.hasExpiredInboundGap(timeout) {
|
||||
continue
|
||||
}
|
||||
c.log.Warnf(
|
||||
"<yellow>socks_id=<cyan>%d</cyan> inbound reorder gap expired, closing connection</yellow>",
|
||||
socksConn.ID,
|
||||
)
|
||||
socksConn.ConnectFailure = "inbound reorder timeout"
|
||||
socksConn.ResetTransportState()
|
||||
_ = socksConn.CloseLocal()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *sendWorker) postBatch(ctx context.Context, c *Client, batch protocol.Batch, body []byte) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.RelayURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
@@ -354,6 +371,42 @@ func (c *Client) applyResponsePacket(packet protocol.Packet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isReorderSequencedPacket(packet.Type) {
|
||||
readyPackets, duplicate, overflow := socksConn.queueInboundPacket(packet, c.cfg.MaxReorderBufferPackets)
|
||||
if duplicate {
|
||||
c.log.Debugf(
|
||||
"<gray>ignored duplicate inbound packet socks_id=<cyan>%d</cyan> type=<cyan>%s</cyan> seq=<cyan>%d</cyan></gray>",
|
||||
socksConn.ID, packet.Type, packet.Sequence,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
if overflow {
|
||||
c.log.Warnf(
|
||||
"<yellow>inbound reorder buffer overflow socks_id=<cyan>%d</cyan> type=<cyan>%s</cyan> seq=<cyan>%d</cyan></yellow>",
|
||||
socksConn.ID, packet.Type, packet.Sequence,
|
||||
)
|
||||
socksConn.ConnectFailure = "inbound reorder overflow"
|
||||
socksConn.ResetTransportState()
|
||||
_ = socksConn.CloseLocal()
|
||||
return nil
|
||||
}
|
||||
for _, readyPacket := range readyPackets {
|
||||
if err := c.applyOrderedResponsePacket(socksConn, readyPacket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.applyOrderedResponsePacket(socksConn, packet)
|
||||
}
|
||||
|
||||
func (c *Client) applyOrderedResponsePacket(socksConn *SOCKSConnection, packet protocol.Packet) error {
|
||||
switch packet.Type {
|
||||
case protocol.PacketTypePing, protocol.PacketTypePong:
|
||||
return nil
|
||||
}
|
||||
|
||||
switch packet.Type {
|
||||
case protocol.PacketTypeSOCKSConnectAck:
|
||||
_ = socksConn.AckPacket(packet)
|
||||
@@ -364,6 +417,11 @@ func (c *Client) applyResponsePacket(packet protocol.Packet) error {
|
||||
socksConn.ID,
|
||||
)
|
||||
socksConn.CompleteConnect(nil)
|
||||
for _, readyPacket := range socksConn.activateInboundDrain() {
|
||||
if err := c.applyOrderedResponsePacket(socksConn, readyPacket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case protocol.PacketTypeSOCKSConnectFail,
|
||||
|
||||
@@ -3,8 +3,10 @@ package client
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"masterhttprelayvpn/internal/config"
|
||||
"masterhttprelayvpn/internal/protocol"
|
||||
)
|
||||
|
||||
func TestSOCKSConnectionStoreDeleteClearsTransportState(t *testing.T) {
|
||||
@@ -66,6 +68,87 @@ func TestSOCKSConnectionStoreDeleteClearsTransportState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSOCKSConnectionInboundReorderQueuesAndDrainsInOrder(t *testing.T) {
|
||||
socksConn := &SOCKSConnection{
|
||||
ConnectAccepted: true,
|
||||
PendingInbound: make(map[uint64]PendingInboundPacket),
|
||||
}
|
||||
|
||||
packet2 := protocol.NewPacket("client-session", protocol.PacketTypeSOCKSData)
|
||||
packet2.SOCKSID = 1
|
||||
packet2.Sequence = 2
|
||||
packet2.Payload = []byte("two")
|
||||
|
||||
ready, duplicate, overflow := socksConn.queueInboundPacket(packet2, 8)
|
||||
if duplicate || overflow {
|
||||
t.Fatalf("unexpected duplicate=%t overflow=%t", duplicate, overflow)
|
||||
}
|
||||
if len(ready) != 0 {
|
||||
t.Fatalf("expected no ready packets before gap is filled, got %d", len(ready))
|
||||
}
|
||||
|
||||
packet1 := protocol.NewPacket("client-session", protocol.PacketTypeSOCKSData)
|
||||
packet1.SOCKSID = 1
|
||||
packet1.Sequence = 1
|
||||
packet1.Payload = []byte("one")
|
||||
|
||||
ready, duplicate, overflow = socksConn.queueInboundPacket(packet1, 8)
|
||||
if duplicate || overflow {
|
||||
t.Fatalf("unexpected duplicate=%t overflow=%t", duplicate, overflow)
|
||||
}
|
||||
if len(ready) != 2 {
|
||||
t.Fatalf("expected 2 ready packets after filling gap, got %d", len(ready))
|
||||
}
|
||||
if ready[0].Sequence != 1 || ready[1].Sequence != 2 {
|
||||
t.Fatalf("expected ordered sequences [1 2], got [%d %d]", ready[0].Sequence, ready[1].Sequence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSOCKSConnectionInboundGapTimeout(t *testing.T) {
|
||||
socksConn := &SOCKSConnection{
|
||||
PendingInbound: make(map[uint64]PendingInboundPacket),
|
||||
}
|
||||
socksConn.PendingInbound[5] = PendingInboundPacket{
|
||||
Packet: protocol.Packet{Sequence: 5},
|
||||
QueuedAt: time.Now().Add(-2 * time.Second),
|
||||
}
|
||||
|
||||
if !socksConn.hasExpiredInboundGap(500 * time.Millisecond) {
|
||||
t.Fatal("expected inbound gap timeout to trigger")
|
||||
}
|
||||
if len(socksConn.PendingInbound) != 0 {
|
||||
t.Fatalf("expected pending inbound buffer to be cleared, got %d items", len(socksConn.PendingInbound))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSOCKSConnectionInboundDataWaitsForConnectAck(t *testing.T) {
|
||||
socksConn := &SOCKSConnection{
|
||||
PendingInbound: make(map[uint64]PendingInboundPacket),
|
||||
}
|
||||
|
||||
packet1 := protocol.NewPacket("client-session", protocol.PacketTypeSOCKSData)
|
||||
packet1.SOCKSID = 1
|
||||
packet1.Sequence = 1
|
||||
packet1.Payload = []byte("one")
|
||||
|
||||
ready, duplicate, overflow := socksConn.queueInboundPacket(packet1, 8)
|
||||
if duplicate || overflow {
|
||||
t.Fatalf("unexpected duplicate=%t overflow=%t", duplicate, overflow)
|
||||
}
|
||||
if len(ready) != 0 {
|
||||
t.Fatalf("expected buffered packet before connect ack, got %d ready packets", len(ready))
|
||||
}
|
||||
|
||||
socksConn.ConnectAccepted = true
|
||||
ready = socksConn.activateInboundDrain()
|
||||
if len(ready) != 1 {
|
||||
t.Fatalf("expected 1 ready packet after connect ack, got %d", len(ready))
|
||||
}
|
||||
if ready[0].Sequence != 1 {
|
||||
t.Fatalf("expected sequence 1, got %d", ready[0].Sequence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNextBatchRotatesAcrossConnections(t *testing.T) {
|
||||
cfg := config.Config{
|
||||
MaxChunkSize: 1024,
|
||||
@@ -89,14 +172,19 @@ func TestBuildNextBatchRotatesAcrossConnections(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
expected := []uint64{conn1.ID, conn2.ID, conn3.ID}
|
||||
for i, want := range expected {
|
||||
seen := make(map[uint64]bool)
|
||||
for i := 0; i < 3; i++ {
|
||||
batch, selected := client.buildNextBatch()
|
||||
if len(batch.Packets) != 1 || len(selected) != 1 {
|
||||
t.Fatalf("iteration %d: expected one selected packet, got packets=%d selected=%d", i, len(batch.Packets), len(selected))
|
||||
}
|
||||
if got := batch.Packets[0].SOCKSID; got != want {
|
||||
t.Fatalf("iteration %d: expected socks_id=%d, got %d", i, want, got)
|
||||
got := batch.Packets[0].SOCKSID
|
||||
if seen[got] {
|
||||
t.Fatalf("iteration %d: duplicate socks_id=%d selected before all queues were drained", i, got)
|
||||
}
|
||||
seen[got] = true
|
||||
}
|
||||
if len(seen) != 3 {
|
||||
t.Fatalf("expected all 3 socks connections to be selected once, got %d unique selections", len(seen))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"masterhttprelayvpn/internal/protocol"
|
||||
)
|
||||
|
||||
type SOCKSConnection struct {
|
||||
@@ -40,6 +42,7 @@ type SOCKSConnection struct {
|
||||
LocalConn net.Conn
|
||||
localWriteMu sync.Mutex
|
||||
localCloseMu sync.Mutex
|
||||
reorderMu sync.Mutex
|
||||
localReadEOF bool
|
||||
localWriteEOF bool
|
||||
closedC chan struct{}
|
||||
@@ -49,6 +52,13 @@ type SOCKSConnection struct {
|
||||
OutboundQueue []*SOCKSOutboundQueueItem
|
||||
QueuedBytes int
|
||||
InFlight map[string]*SOCKSOutboundQueueItem
|
||||
NextInboundSequence uint64
|
||||
PendingInbound map[uint64]PendingInboundPacket
|
||||
}
|
||||
|
||||
type PendingInboundPacket struct {
|
||||
Packet protocol.Packet
|
||||
QueuedAt time.Time
|
||||
}
|
||||
|
||||
func (s *SOCKSConnection) InitialPayloadHex() string {
|
||||
@@ -83,6 +93,7 @@ func (s *SOCKSConnectionStore) New(clientSessionKey string, clientAddress string
|
||||
closedC: make(chan struct{}),
|
||||
connectResultC: make(chan error, 1),
|
||||
InFlight: make(map[string]*SOCKSOutboundQueueItem),
|
||||
PendingInbound: make(map[uint64]PendingInboundPacket),
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -197,6 +208,10 @@ func (s *SOCKSConnection) ResetTransportState() {
|
||||
|
||||
s.InitialPayload = nil
|
||||
s.BufferedBytes = 0
|
||||
s.reorderMu.Lock()
|
||||
clear(s.PendingInbound)
|
||||
s.NextInboundSequence = 0
|
||||
s.reorderMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *SOCKSConnectionStore) Get(id uint64) *SOCKSConnection {
|
||||
|
||||
Reference in New Issue
Block a user