mirror of
https://github.com/soypat/lneto.git
synced 2026-08-11 02:13:44 +00:00
add SYNCookie implementation (unused)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user