add SYNCookie implementation (unused)

This commit is contained in:
soypat
2026-01-08 11:25:11 -03:00
parent 2e8b3e2ddd
commit 7cbf0fb301
2 changed files with 498 additions and 0 deletions
+245
View File
@@ -0,0 +1,245 @@
package tcp
import (
"encoding/binary"
"errors"
"io"
)
// Embed low 5 bits of counter into cookie for efficient validation.
// Lower bits of cookie are counter bits.
const (
cookiebits = 32
counterbits = 5
hashbits = cookiebits - counterbits
countermsk = (1 << counterbits) - 1
)
// SYNCookieJar implements SYN cookie generation and validation for TCP SYN flood protection.
// SYN cookies allow a server to avoid allocating state for half-open connections by
// encoding connection parameters into the Initial Sequence Number (ISS) of the SYN-ACK response.
//
// The cookie encodes:
// - A hash of the connection tuple (src IP, dst IP, src port, dst port)
// - A timestamp counter for cookie expiration
// - MSS index (optional, for preserving Maximum Segment Size negotiation)
//
// See RFC 4987 for background on SYN flood attacks and cookie-based mitigations.
type SYNCookieJar struct {
// counter is incremented periodically or under pressure to expire old cookies.
// Cookies generated with a counter more than maxCounterDelta behind current are rejected.
counter uint32
// maxCounterDelta defines how many counter increments a cookie remains valid.
// A value of 2 means cookies from counter, counter-1, and counter-2 are accepted.
maxCounterDelta uint32
// secret is the key used for cookie generation. Should be random and kept private.
secret [16]byte
}
// SYNCookieConfig contains configuration for SYN cookie initialization.
type SYNCookieConfig struct {
// Rand is used for entropy generation of cookies.
Rand io.Reader
// MaxCounterDelta defines cookie validity window in counter increments.
// Recommended value is 1-2. Zero defaults to 1.
MaxCounterDelta uint32
}
var (
errInvalidCookie = errors.New("tcp: invalid SYN cookie")
)
// Reset initializes or reinitializes the SYNCookie with the given configuration.
// The counter is preserved across resets to maintain cookie validity during secret rotation.
func (sc *SYNCookieJar) Reset(config SYNCookieConfig) error {
if config.Rand == nil {
return errors.New("need rand function")
}
_, err := io.ReadFull(config.Rand, sc.secret[:])
if err != nil {
return err
}
maxDelta := config.MaxCounterDelta
if maxDelta == 0 {
maxDelta = 1
}
sc.maxCounterDelta = maxDelta
// counter is intentionally NOT reset to preserve validity of recent cookies
return nil
}
// IncrementCounter advances the counter, which will eventually expire old cookies.
// Call this periodically (e.g., every few seconds) or when under SYN flood pressure.
func (sc *SYNCookieJar) IncrementCounter() {
sc.counter++
}
// Counter returns the current counter value.
func (sc *SYNCookieJar) Counter() uint32 {
return sc.counter
}
// MakeSYNCookie creates a SYN cookie value to be used as the ISS in a SYN-ACK response.
// The cookie encodes the connection tuple and current counter for later validation.
//
// Parameters:
// - srcAddr: source IP address (4 bytes for IPv4, 16 for IPv6)
// - dstAddr: destination IP address
// - srcPort: source TCP port
// - dstPort: destination TCP port
// - clientISN: the client's Initial Sequence Number from the SYN packet
func (sc *SYNCookieJar) MakeSYNCookie(srcAddr, dstAddr []byte, srcPort, dstPort uint16, clientISN Value) Value {
return sc.generateWithCounter(srcAddr, dstAddr, srcPort, dstPort, clientISN, sc.counter)
}
// generateWithCounter creates a cookie using a specific counter value.
func (sc *SYNCookieJar) generateWithCounter(srcAddr, dstAddr []byte, srcPort, dstPort uint16, clientISN Value, counter uint32) Value {
// Cookie structure (32 bits):
// [5 bits: counter low bits][27 bits: hash of tuple+secret+counter]
//
// The counter bits allow validation to check multiple counter values efficiently.
// The hash provides cryptographic binding to the connection tuple.
hash := sc.hashTuple(srcAddr, dstAddr, srcPort, dstPort, clientISN, counter)
hash = hash << counterbits
return Value(hash | counter&countermsk)
}
// ValidateSYNCookie checks if an ACK number from a client completing the handshake contains
// a valid cookie. Returns the original cookie value if valid.
//
// Parameters:
// - srcAddr, dstAddr: IP addresses (must match original SYN)
// - srcPort, dstPort: TCP ports (must match original SYN)
// - clientISN: client's ISN from original SYN (can be derived from ack-1 of final ACK)
// - ackNum: the ACK number from the client's ACK packet (should be cookie+1)
//
// Returns the cookie value and nil error if valid, or zero and error if invalid.
func (sc *SYNCookieJar) ValidateSYNCookie(srcAddr, dstAddr []byte, srcPort, dstPort uint16, clientISN Value, ackNum Value) (Value, error) {
// Client ACKs cookie+1, so the cookie is ackNum-1
cookie := ackNum - 1
// Extract counter bits from cookie
cookieCounterBits := uint32(cookie) & countermsk
// Try validation with current counter and allowed previous values
for delta := uint32(0); delta <= sc.maxCounterDelta; delta++ {
tryCounter := sc.counter - delta
tryCounterBits := tryCounter & countermsk
if tryCounterBits != cookieCounterBits {
continue
}
// Counter bits match, verify full hash
expected := sc.generateWithCounter(srcAddr, dstAddr, srcPort, dstPort, clientISN, tryCounter)
if expected == cookie {
return cookie, nil
}
}
return 0, errInvalidCookie
}
// hashTuple computes a hash of the connection tuple mixed with secret and counter.
// Uses a simple but effective mixing function suitable for embedded systems.
func (sc *SYNCookieJar) hashTuple(srcAddr, dstAddr []byte, srcPort, dstPort uint16, clientISN Value, counter uint32) uint32 {
// Initialize with secret words
h0 := binary.LittleEndian.Uint32(sc.secret[0:4])
h1 := binary.LittleEndian.Uint32(sc.secret[4:8])
h2 := binary.LittleEndian.Uint32(sc.secret[8:12])
h3 := binary.LittleEndian.Uint32(sc.secret[12:16])
// Mix in connection tuple
h0 ^= uint32(srcPort) | (uint32(dstPort) << 16)
h1 ^= uint32(clientISN)
h2 ^= counter
// Mix in addresses (handle both IPv4 and IPv6)
for i := 0; i+3 < len(srcAddr); i += 4 {
h3 ^= binary.LittleEndian.Uint32(srcAddr[i:])
h0, h1, h2, h3 = mixRound(h0, h1, h2, h3)
}
// Handle remaining bytes of srcAddr
if rem := len(srcAddr) % 4; rem != 0 {
var last uint32
for i := 0; i < rem; i++ {
last |= uint32(srcAddr[len(srcAddr)-rem+i]) << (i * 8)
}
h3 ^= last
}
for i := 0; i+3 < len(dstAddr); i += 4 {
h0 ^= binary.LittleEndian.Uint32(dstAddr[i:])
h0, h1, h2, h3 = mixRound(h0, h1, h2, h3)
}
// Handle remaining bytes of dstAddr
if rem := len(dstAddr) % 4; rem != 0 {
var last uint32
for i := 0; i < rem; i++ {
last |= uint32(dstAddr[len(dstAddr)-rem+i]) << (i * 8)
}
h0 ^= last
}
// Final mixing rounds
h0, h1, h2, h3 = mixRound(h0, h1, h2, h3)
h0, h1, h2, h3 = mixRound(h0, h1, h2, h3)
return h0 ^ h1 ^ h2 ^ h3
}
// mixRound performs one round of mixing, similar to SipHash quarter-round.
func mixRound(a, b, c, d uint32) (uint32, uint32, uint32, uint32) {
a += b
d ^= a
d = rotl32(d, 16)
c += d
b ^= c
b = rotl32(b, 12)
a += b
d ^= a
d = rotl32(d, 8)
c += d
b ^= c
b = rotl32(b, 7)
return a, b, c, d
}
// rotl32 performs a 32-bit left rotation.
func rotl32(x uint32, n int) uint32 {
return (x << n) | (x >> (32 - n))
}
// encodeMSSIndex encodes an MSS value into a 2-bit index for embedding in cookies.
// Common MSS values are mapped to indices 0-3. Returns the closest match.
func encodeMSSIndex(mss uint16) uint8 {
// Common MSS values: 536 (minimum), 1460 (Ethernet), 1440 (PPPoE), 8960 (jumbo)
switch {
case mss <= 536:
return 0
case mss <= 1220:
return 1
case mss <= 1460:
return 2
default:
return 3
}
}
// decodeMSSIndex converts a 2-bit index back to an MSS value.
func decodeMSSIndex(idx uint8) uint16 {
switch idx & 0x3 {
case 0:
return 536
case 1:
return 1220
case 2:
return 1460
default:
return 8960
}
}
+253
View File
@@ -0,0 +1,253 @@
package tcp
import (
"math/rand"
"testing"
)
func TestSYNCookie_ResetValidation(t *testing.T) {
var sc SYNCookieJar
// Zero secret should fail
err := sc.Reset(SYNCookieConfig{})
if err == nil {
t.Errorf("expected error, got %v", err)
}
rng := rand.New(rand.NewSource(1))
// Valid secret should succeed
err = sc.Reset(SYNCookieConfig{Rand: rng})
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
}
func TestSYNCookie_GenerateValidate(t *testing.T) {
var sc SYNCookieJar
rng := rand.New(rand.NewSource(1))
err := sc.Reset(SYNCookieConfig{Rand: rng, MaxCounterDelta: 2})
if err != nil {
t.Fatal(err)
}
srcAddr := []byte{192, 168, 1, 100}
dstAddr := []byte{10, 0, 0, 1}
srcPort := uint16(54321)
dstPort := uint16(80)
clientISN := Value(0x12345678)
// Generate cookie
cookie := sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
// Validate with ACK = cookie + 1
ackNum := cookie + 1
validatedCookie, err := sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
if err != nil {
t.Errorf("expected valid cookie, got error: %v", err)
}
if validatedCookie != cookie {
t.Errorf("expected cookie %d, got %d", cookie, validatedCookie)
}
}
func TestSYNCookie_CounterExpiration(t *testing.T) {
var sc SYNCookieJar
rng := rand.New(rand.NewSource(1))
err := sc.Reset(SYNCookieConfig{Rand: rng, MaxCounterDelta: 1})
if err != nil {
t.Fatal(err)
}
srcAddr := []byte{192, 168, 1, 100}
dstAddr := []byte{10, 0, 0, 1}
srcPort := uint16(54321)
dstPort := uint16(80)
clientISN := Value(0x12345678)
// Generate cookie at counter=0
cookie := sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
ackNum := cookie + 1
// Should validate at counter=0
_, err = sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
if err != nil {
t.Errorf("expected valid at counter=0, got: %v", err)
}
// Increment counter once - should still validate (within delta=1)
sc.IncrementCounter()
_, err = sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
if err != nil {
t.Errorf("expected valid at counter=1, got: %v", err)
}
// Increment again - should still validate (counter=2, cookie from 0, delta=1 allows 1 and 2)
sc.IncrementCounter()
_, err = sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
if err == nil {
t.Errorf("expected cookie to be expired at counter=2 with delta=1")
}
}
func TestSYNCookie_DifferentTuples(t *testing.T) {
var sc SYNCookieJar
rng := rand.New(rand.NewSource(1))
err := sc.Reset(SYNCookieConfig{Rand: rng})
if err != nil {
t.Fatal(err)
}
srcAddr := []byte{192, 168, 1, 100}
dstAddr := []byte{10, 0, 0, 1}
srcPort := uint16(54321)
dstPort := uint16(80)
clientISN := Value(0x12345678)
cookie := sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
ackNum := cookie + 1
// Different source address should fail
wrongSrcAddr := []byte{192, 168, 1, 101}
_, err = sc.ValidateSYNCookie(wrongSrcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
if err == nil {
t.Error("expected error for wrong source address")
}
// Different port should fail
_, err = sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort+1, dstPort, clientISN, ackNum)
if err == nil {
t.Error("expected error for wrong source port")
}
// Different clientISN should fail
_, err = sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN+1, ackNum)
if err == nil {
t.Error("expected error for wrong client ISN")
}
// Correct tuple should succeed
_, err = sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
if err != nil {
t.Errorf("expected success for correct tuple, got: %v", err)
}
}
func TestSYNCookie_IPv6(t *testing.T) {
var sc SYNCookieJar
rng := rand.New(rand.NewSource(1))
err := sc.Reset(SYNCookieConfig{Rand: rng})
if err != nil {
t.Fatal(err)
}
// IPv6 addresses
srcAddr := []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
dstAddr := []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}
srcPort := uint16(54321)
dstPort := uint16(443)
clientISN := Value(0xDEADBEEF)
cookie := sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
ackNum := cookie + 1
validatedCookie, err := sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
if err != nil {
t.Errorf("expected valid IPv6 cookie, got error: %v", err)
}
if validatedCookie != cookie {
t.Errorf("expected cookie %d, got %d", cookie, validatedCookie)
}
}
func TestSYNCookie_Deterministic(t *testing.T) {
var sc SYNCookieJar
rng := rand.New(rand.NewSource(1))
err := sc.Reset(SYNCookieConfig{Rand: rng})
if err != nil {
t.Fatal(err)
}
srcAddr := []byte{192, 168, 1, 100}
dstAddr := []byte{10, 0, 0, 1}
srcPort := uint16(54321)
dstPort := uint16(80)
clientISN := Value(0x12345678)
// Same inputs should produce same cookie
cookie1 := sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
cookie2 := sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
if cookie1 != cookie2 {
t.Errorf("expected deterministic cookies: %d != %d", cookie1, cookie2)
}
}
func TestMSSIndexEncoding(t *testing.T) {
tests := []struct {
mss uint16
expectedIdx uint8
}{
{200, 0},
{536, 0},
{537, 1},
{1220, 1},
{1221, 2},
{1460, 2},
{1461, 3},
{8960, 3},
{9000, 3},
}
for _, tc := range tests {
idx := encodeMSSIndex(tc.mss)
if idx != tc.expectedIdx {
t.Errorf("EncodeMSSIndex(%d) = %d, want %d", tc.mss, idx, tc.expectedIdx)
}
}
// Test round-trip for decoded values
for idx := uint8(0); idx <= 3; idx++ {
mss := decodeMSSIndex(idx)
reIdx := encodeMSSIndex(mss)
if reIdx != idx {
t.Errorf("MSS index round-trip failed: %d -> %d -> %d", idx, mss, reIdx)
}
}
}
func BenchmarkSYNCookie_Generate(b *testing.B) {
var sc SYNCookieJar
rng := rand.New(rand.NewSource(1))
sc.Reset(SYNCookieConfig{Rand: rng})
srcAddr := []byte{192, 168, 1, 100}
dstAddr := []byte{10, 0, 0, 1}
srcPort := uint16(54321)
dstPort := uint16(80)
clientISN := Value(0x12345678)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
}
}
func BenchmarkSYNCookie_Validate(b *testing.B) {
var sc SYNCookieJar
rng := rand.New(rand.NewSource(1))
sc.Reset(SYNCookieConfig{Rand: rng, MaxCounterDelta: 2})
srcAddr := []byte{192, 168, 1, 100}
dstAddr := []byte{10, 0, 0, 1}
srcPort := uint16(54321)
dstPort := uint16(80)
clientISN := Value(0x12345678)
cookie := sc.MakeSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN)
ackNum := cookie + 1
b.ResetTimer()
for i := 0; i < b.N; i++ {
sc.ValidateSYNCookie(srcAddr, dstAddr, srcPort, dstPort, clientISN, ackNum)
}
}