Retransmit active-open SYN after RTO (#130)

* added: sync packet retransmission

* test: sync packet retransmission

* refactor: remove packet time from tcppool

* fix: return comment

* refactor: move retrying to stackRetrying

* added: retries & timeout to stackGoConfig

* test: test retries stack

* refactor: move StackRetrying to stackBlocking

* fix: line gofmt

---------

Co-authored-by: Pat Whittingslow <graded.sp@gmail.com>
This commit is contained in:
TuteMthCD
2026-06-23 00:19:16 -03:00
committed by GitHub
parent a0a5336108
commit d5ea2efdf4
7 changed files with 256 additions and 15 deletions
+8 -2
View File
@@ -181,6 +181,14 @@ func (s StackBlocking) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.A
if err != nil {
return err
}
err = s.waitDialTCP(conn, timeout)
if err != nil {
conn.Abort()
}
return err
}
func (s StackBlocking) waitDialTCP(conn *tcp.Conn, timeout time.Duration) (err error) {
deadline := time.Now().Add(timeout)
var backoffs uint
for range maxIter {
@@ -189,12 +197,10 @@ func (s StackBlocking) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.A
return nil
} else if state == tcp.StateSynSent || state == tcp.StateSynRcvd || conn.AwaitingSynSend() {
if err = s.checkDeadline(deadline); err != nil {
conn.Abort()
return err
}
} else {
// Unexpected state, abort and terminate connection.
conn.Abort()
return errTCPFailedToConnect
}
s.backoff(backoffs)
+24 -5
View File
@@ -18,10 +18,15 @@ import (
const (
sockSTREAM = 0x1
sockDGRAM = 0x2
defaultTCPDialTimeout = 2 * time.Second
defaultTCPDialRetries = 1
)
type StackGoConfig struct {
ListenerPoolConfig TCPPoolConfig
TCPDialTimeout time.Duration
TCPDialRetries int
}
func (s *StackAsync) StackGo(stackProtoBackoff lneto.BackoffStrategy, cfg StackGoConfig) StackGo {
@@ -32,16 +37,30 @@ func (s *StackAsync) StackGo(stackProtoBackoff lneto.BackoffStrategy, cfg StackG
}
func (s StackBlocking) StackGo(cfg StackGoConfig) StackGo {
tcpDialTimeout := cfg.TCPDialTimeout
tcpDialRetries := cfg.TCPDialRetries
// Defaults
if tcpDialTimeout <= 0 {
tcpDialTimeout = defaultTCPDialTimeout
}
if tcpDialRetries <= 0 {
tcpDialRetries = defaultTCPDialRetries
}
sg := StackGo{
blk: s,
plcfg: cfg.ListenerPoolConfig,
blk: s,
plcfg: cfg.ListenerPoolConfig,
tcpDialTimeout: tcpDialTimeout,
tcpDialRetries: tcpDialRetries,
}
return sg
}
type StackGo struct {
blk StackBlocking
plcfg TCPPoolConfig
blk StackBlocking
plcfg TCPPoolConfig
tcpDialTimeout time.Duration
tcpDialRetries int
}
func (s StackGo) Socket(ctx context.Context, network string, family, sotype int, laddr, raddr net.Addr) (c any, err error) {
@@ -171,7 +190,7 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
if err != nil {
return nil, err
}
err = s.blk.async.DialTCP(&conn, laddr.Port(), raddr)
err = s.blk.StackRetrying().DoDialTCP(&conn, laddr.Port(), raddr, s.tcpDialTimeout, s.tcpDialRetries)
if err != nil {
return nil, err
}
+22 -5
View File
@@ -13,9 +13,11 @@ func (s *StackAsync) StackRetrying(stackProtoBackoff lneto.BackoffStrategy) Stac
if stackProtoBackoff == nil {
panic("nil backoff to StackRetrying")
}
return StackRetrying{
block: s.StackBlocking(stackProtoBackoff),
}
return s.StackBlocking(stackProtoBackoff).StackRetrying()
}
func (s StackBlocking) StackRetrying() StackRetrying {
return StackRetrying{block: s}
}
var (
@@ -93,13 +95,28 @@ func (s StackRetrying) DoResolveHardwareAddress6(addr netip.Addr, timeout time.D
func (s StackRetrying) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.AddrPort, timeout time.Duration, retries int) (err error) {
expectEnd := time.Now().Add(timeout * time.Duration(retries))
var firstErr error
for range retries {
err = s.block.DoDialTCP(conn, localPort, addrp, timeout)
for i := range retries {
if i == 0 || conn.State().IsClosed() {
err = s.block.async.DialTCP(conn, localPort, addrp)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
} else if conn.IsAwaitingControl() {
conn.RequeueControl()
}
err = s.block.waitDialTCP(conn, timeout)
if err == nil {
return nil
} else if firstErr == nil {
firstErr = err
}
if !conn.IsAwaitingControl() {
conn.Abort()
}
}
if time.Now().Before(expectEnd) {
if err != firstErr {
+53
View File
@@ -2,10 +2,12 @@ package xnet
import (
"bytes"
"context"
"errors"
"math/rand"
"net/netip"
"sync"
"syscall"
"testing"
"time"
@@ -105,6 +107,57 @@ func TestTCPConn_ReadBlocksUntilDataAvailable(t *testing.T) {
}
}
func TestStackGoTCPDialRetriesPendingControl(t *testing.T) {
const seed = 5678
const MTU = ethernet.MaxMTU
client, sv, _, _ := newTCPStacks(t, seed, MTU)
sg := client.StackBlocking(backoffYield).StackGo(StackGoConfig{
ListenerPoolConfig: TCPPoolConfig{
QueueSize: 4,
TxBufSize: MTU,
RxBufSize: MTU,
NewBackoff: func() lneto.BackoffStrategy {
return backoffYield
},
},
TCPDialTimeout: 10 * time.Millisecond,
TCPDialRetries: 2,
})
laddr := netip.AddrPortFrom(netip.AddrFrom4(client.Addr4()), 1234)
raddr := netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), 22)
done := make(chan error, 1)
go func() {
_, err := sg.SocketNetip(context.Background(), "tcp", syscall.AF_INET, sockSTREAM, laddr, raddr)
done <- err
}()
var buf [ethernet.MaxMTU + ethernet.MaxOverheadSize]byte
waitForEgress := func() {
t.Helper()
deadline := time.Now().Add(100 * time.Millisecond)
for time.Now().Before(deadline) {
n, err := client.EgressEthernet(buf[:])
if err != nil {
t.Fatal(err)
}
if n > 0 {
return
}
time.Sleep(time.Millisecond)
}
t.Fatal("timed out waiting for TCP dial egress packet")
}
waitForEgress()
waitForEgress()
err := <-done
if err == nil {
t.Fatal("expected TCP dial to fail after retries without peer response")
}
}
func TestStackAsyncTCP_multipacket(t *testing.T) {
const seed = 1234
const MTU = 512