tcp: mss honoring; accept syn with ECE/CWR flags; add RSTQueue type (#41)

* tcp: mss honoring; accept syn with ECE/CWR flags; add RSTQueue type

* tcp: move option logic to own file

* remove prints in pcap

* add MSS send threshold inspired by linux/freebsd/lwip thresh
This commit is contained in:
Pat Whittingslow
2026-02-25 16:58:55 +01:00
committed by GitHub
parent 58a9bf57e5
commit be949d960c
10 changed files with 570 additions and 191 deletions
-2
View File
@@ -229,7 +229,6 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
if err == nil {
tfrm.ValidateSize(pc.validator())
if pc.vld.HasError() {
println("BAD TCP")
return dst, pc.vld.ErrPop()
}
ifrm4.CRCWriteTCPPseudo(&crc)
@@ -242,7 +241,6 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
if err == nil {
ufrm.ValidateSize(pc.validator())
if pc.vld.HasError() {
println("BAD UDP")
return dst, pc.vld.ErrPop()
}
if ufrm.CRC() != 0 {
+22 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/tcp"
)
type StackPorts struct {
@@ -18,7 +19,8 @@ type StackPorts struct {
handlers handlers
dstPortOff uint16
protocol uint16
// stores last node to demux/encapsulate.
// rstQueue stores pending RST responses for TCP SYNs to unregistered ports.
rstQueue tcp.RSTQueue
}
func (ps *StackPorts) ResetUDP(maxNodes int) error {
@@ -56,6 +58,9 @@ func (ps *StackPorts) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame
return 0, io.ErrShortBuffer
}
_, n, err = ps.handlers.encapsulateAny(carrierData, offsetToIP, offsetToFrame)
if n == 0 {
n, _ = ps.rstQueue.Drain(carrierData, offsetToIP, offsetToFrame)
}
return n, err
}
@@ -65,6 +70,19 @@ func (ps *StackPorts) Demux(b []byte, offset int) (err error) {
}
port := binary.BigEndian.Uint16(b[int(ps.dstPortOff)+offset:])
_, err = ps.handlers.demuxByPort(b, offset, port)
if err == lneto.ErrPacketDrop && ps.protocol == uint16(lneto.IPProtoTCP) && offset+14 <= len(b) {
// RFC 9293 §3.10.7.1: RST for SYN to port with no listener.
flags := binary.BigEndian.Uint16(b[offset+12:]) & 0x01ff
const flagSYN, flagRST, flagACK = 0x02, 0x04, 0x10
if flags&flagSYN != 0 && flags&(flagRST|flagACK) == 0 {
srcaddr, _, _, _, iperr := internal.GetIPAddr(b)
if iperr == nil {
remotePort := binary.BigEndian.Uint16(b[offset:])
segSeq := tcp.Value(binary.BigEndian.Uint32(b[offset+4:]))
ps.rstQueue.Queue(srcaddr, remotePort, port, 0, segSeq+1, tcp.FlagRST|tcp.FlagACK)
}
}
}
return err
}
@@ -149,5 +167,8 @@ func (ps *StackPortsMACFiltered) Encapsulate(carrierData []byte, offsetToIP, off
h.error("handlers:encapsulate", slog.String("func", "encapsulateAny"), slog.String("ctx", h.context), slog.String("err", err.Error()))
}
}
if n, _ := ps.sp.rstQueue.Drain(carrierData, offsetToIP, offsetToFrame); n > 0 {
return n, nil
}
return 0, err // Return last written error.
}
+215
View File
@@ -1,6 +1,7 @@
package internet
import (
"encoding/binary"
"math/rand"
"net/netip"
"testing"
@@ -390,6 +391,220 @@ func TestListener_RSTOnPoolExhaustion(t *testing.T) {
}
}
func TestListener_RSTOnStalePacket(t *testing.T) {
// Test Scenario C: stale FIN,ACK to a port with a listener but no matching connection.
// Test at Listener level directly to avoid StackIP CRC validation.
var listener tcp.Listener
pool := newMockTCPPool(1, 3, 2048)
serverPort := uint16(80)
if err := listener.Reset(serverPort, pool); err != nil {
t.Fatal(err)
}
// Build a raw stale FIN,ACK targeting port 80 from an unknown source.
// IP header (20 bytes) + TCP header (20 bytes).
clientIP := [4]byte{10, 0, 0, 1}
serverIP := [4]byte{10, 0, 0, 2}
rawBuf := make([]byte, 256)
rawBuf[0] = 0x45 // version=4, IHL=5
rawBuf[9] = 6 // protocol=TCP
copy(rawBuf[12:16], clientIP[:])
copy(rawBuf[16:20], serverIP[:])
// TCP header at offset 20.
binary.BigEndian.PutUint16(rawBuf[20:], 1337) // src port
binary.BigEndian.PutUint16(rawBuf[22:], serverPort) // dst port
binary.BigEndian.PutUint32(rawBuf[24:], 500) // SEQ
binary.BigEndian.PutUint32(rawBuf[28:], 200) // ACK
rawBuf[32] = 0x50 // offset=5
rawBuf[33] = 0x11 // flags = FIN|ACK (0x01|0x10)
// Demux directly on Listener — no matching connection, should queue RST.
err := listener.Demux(rawBuf[:40], 20)
if err == nil {
t.Fatal("expected error from stale FIN,ACK demux")
}
// Encapsulate should produce RST.
// Pre-fill IP version (StackIP normally does this before calling children).
var outBuf [256]byte
outBuf[0] = 0x45
n, err := listener.Encapsulate(outBuf[:], 0, 20)
if err != nil {
t.Fatal("encapsulate RST:", err)
} else if n == 0 {
t.Fatal("no RST produced for stale packet")
}
tfrm, err := tcp.NewFrame(outBuf[20 : 20+n])
if err != nil {
t.Fatal("parse RST frame:", err)
}
_, flags := tfrm.OffsetAndFlags()
if flags != tcp.FlagRST {
t.Errorf("RST flags: got %s, want [RST]", flags)
}
if tfrm.Seq() != 200 {
t.Errorf("RST SEQ: got %d, want 200 (stale packet's ACK)", tfrm.Seq())
}
if tfrm.SourcePort() != serverPort {
t.Errorf("RST source port: got %d, want %d", tfrm.SourcePort(), serverPort)
}
if tfrm.DestinationPort() != 1337 {
t.Errorf("RST dest port: got %d, want 1337", tfrm.DestinationPort())
}
}
func TestStackPorts_RSTOnUnknownPort(t *testing.T) {
// Test Scenario A: SYN to a port with no listener (e.g. HTTPS port 443).
// Test at StackPorts level directly to avoid StackIP CRC validation.
var sp StackPorts
var listener tcp.Listener
pool := newMockTCPPool(1, 3, 2048)
if err := sp.ResetTCP(4); err != nil {
t.Fatal(err)
}
if err := listener.Reset(80, pool); err != nil {
t.Fatal(err)
}
if err := sp.Register(&listener); err != nil {
t.Fatal(err)
}
// Build a SYN to port 443 (no listener).
// StackPorts.Demux expects carrier data starting at IP header with TCP at offset.
clientIP := [4]byte{10, 0, 0, 1}
serverIP := [4]byte{10, 0, 0, 2}
rawBuf := make([]byte, 256)
rawBuf[0] = 0x45 // version=4, IHL=5
rawBuf[9] = 6 // protocol=TCP
copy(rawBuf[12:16], clientIP[:])
copy(rawBuf[16:20], serverIP[:])
// TCP header at offset 20.
binary.BigEndian.PutUint16(rawBuf[20:], 5000) // src port
binary.BigEndian.PutUint16(rawBuf[22:], 443) // dst port (no listener!)
binary.BigEndian.PutUint32(rawBuf[24:], 700) // SEQ = 700
binary.BigEndian.PutUint32(rawBuf[28:], 0) // ACK = 0
rawBuf[32] = 0x50 // offset=5
rawBuf[33] = 0x02 // flags = SYN
err := sp.Demux(rawBuf[:40], 20)
if err == nil {
t.Fatal("expected error for SYN to unknown port")
}
// Pre-fill IP version (StackIP normally does this before calling children).
var outBuf [256]byte
outBuf[0] = 0x45
n, err := sp.Encapsulate(outBuf[:], 0, 20)
if err != nil {
t.Fatal("encapsulate RST:", err)
} else if n == 0 {
t.Fatal("no RST produced for SYN to unknown port")
}
tfrm, err := tcp.NewFrame(outBuf[20 : 20+n])
if err != nil {
t.Fatal("parse RST frame:", err)
}
_, flags := tfrm.OffsetAndFlags()
wantFlags := tcp.FlagRST | tcp.FlagACK
if flags != wantFlags {
t.Errorf("RST flags: got %s, want %s", flags, wantFlags)
}
if tfrm.SourcePort() != 443 {
t.Errorf("RST source port: got %d, want 443", tfrm.SourcePort())
}
if tfrm.DestinationPort() != 5000 {
t.Errorf("RST dest port: got %d, want 5000", tfrm.DestinationPort())
}
if tfrm.Seq() != 0 {
t.Errorf("RST SEQ: got %d, want 0", tfrm.Seq())
}
if tfrm.Ack() != 701 {
t.Errorf("RST ACK: got %d, want 701 (SEG.SEQ+1)", tfrm.Ack())
}
}
func TestListener_ECN_SYN(t *testing.T) {
// Test that Listener.Demux accepts SYN+ECE+CWR (ECN negotiation per RFC 3168).
// Currently fails: strict flags != FlagSYN check rejects ECN SYNs.
var listener tcp.Listener
pool := newMockTCPPool(1, 3, 2048)
serverPort := uint16(80)
if err := listener.Reset(serverPort, pool); err != nil {
t.Fatal(err)
}
// Build SYN+ECE+CWR packet targeting port 80.
clientIP := [4]byte{10, 0, 0, 1}
serverIP := [4]byte{10, 0, 0, 2}
rawBuf := make([]byte, 256)
rawBuf[0] = 0x45 // version=4, IHL=5
rawBuf[9] = 6 // protocol=TCP
copy(rawBuf[12:16], clientIP[:])
copy(rawBuf[16:20], serverIP[:])
// TCP header at offset 20.
binary.BigEndian.PutUint16(rawBuf[20:], 5000) // src port
binary.BigEndian.PutUint16(rawBuf[22:], serverPort)
binary.BigEndian.PutUint32(rawBuf[24:], 300) // SEQ
binary.BigEndian.PutUint32(rawBuf[28:], 0) // ACK
rawBuf[32] = 0x50 // offset=5
rawBuf[33] = byte(tcp.FlagSYN | tcp.FlagECE | tcp.FlagCWR) // SYN+ECE+CWR
// Should be accepted (create connection), not dropped.
err := listener.Demux(rawBuf[:40], 20)
if err != nil {
t.Errorf("SYN+ECE+CWR was rejected: %v (want accepted as valid SYN)", err)
}
}
func TestStackPorts_ECN_SYN_RST(t *testing.T) {
// Test that StackPorts queues RST for SYN+ECE+CWR to an unknown port.
// Currently fails: strict flags == flagSYN check doesn't recognize ECN SYN.
var sp StackPorts
var listener tcp.Listener
pool := newMockTCPPool(1, 3, 2048)
if err := sp.ResetTCP(4); err != nil {
t.Fatal(err)
}
if err := listener.Reset(80, pool); err != nil {
t.Fatal(err)
}
if err := sp.Register(&listener); err != nil {
t.Fatal(err)
}
// Build SYN+ECE+CWR to port 443 (no listener).
clientIP := [4]byte{10, 0, 0, 1}
serverIP := [4]byte{10, 0, 0, 2}
rawBuf := make([]byte, 256)
rawBuf[0] = 0x45
rawBuf[9] = 6
copy(rawBuf[12:16], clientIP[:])
copy(rawBuf[16:20], serverIP[:])
binary.BigEndian.PutUint16(rawBuf[20:], 5000) // src port
binary.BigEndian.PutUint16(rawBuf[22:], 443) // dst port (no listener)
binary.BigEndian.PutUint32(rawBuf[24:], 700) // SEQ
binary.BigEndian.PutUint32(rawBuf[28:], 0) // ACK
rawBuf[32] = 0x50
rawBuf[33] = byte(tcp.FlagSYN | tcp.FlagECE | tcp.FlagCWR)
err := sp.Demux(rawBuf[:40], 20)
if err == nil {
t.Fatal("expected error for SYN to unknown port")
}
// Should have queued RST (same as bare SYN to unknown port).
var outBuf [256]byte
outBuf[0] = 0x45
n, err := sp.Encapsulate(outBuf[:], 0, 20)
if err != nil {
t.Fatal("encapsulate RST:", err)
} else if n == 0 {
t.Error("no RST produced for SYN+ECE+CWR to unknown port (want RST,ACK)")
}
}
// tryExchange attempts an exchange but doesn't fail if no data to send.
func tryExchange(t *testing.T, from, to *StackIP, buf []byte) {
t.Helper()