@@ -485,6 +485,155 @@ func TestTCPAbnormalSequences(t *testing.T) {
485485 })
486486}
487487
488+ // TestTCPPortReuseTombstone verifies that a new connection on a port with a
489+ // tombstoned (closed) conntrack entry is properly tracked. A SYN on a
490+ // tombstoned entry must replace it so that the subsequent SYN-ACK is accepted.
491+ func TestTCPPortReuseTombstone (t * testing.T ) {
492+ srcIP := netip .MustParseAddr ("100.64.0.1" )
493+ dstIP := netip .MustParseAddr ("100.64.0.2" )
494+ srcPort := uint16 (12345 )
495+ dstPort := uint16 (80 )
496+
497+ t .Run ("Outbound port reuse after graceful close" , func (t * testing.T ) {
498+ tracker := NewTCPTracker (DefaultTCPTimeout , logger , flowLogger )
499+ defer tracker .Close ()
500+
501+ key := ConnKey {SrcIP : srcIP , DstIP : dstIP , SrcPort : srcPort , DstPort : dstPort }
502+
503+ // Establish and gracefully close a connection (server-initiated close)
504+ establishConnection (t , tracker , srcIP , dstIP , srcPort , dstPort )
505+
506+ // Server sends FIN
507+ valid := tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPFin | TCPAck , 0 )
508+ require .True (t , valid )
509+
510+ // Client sends FIN-ACK
511+ tracker .TrackOutbound (srcIP , dstIP , srcPort , dstPort , TCPFin | TCPAck , 0 )
512+
513+ // Server sends final ACK
514+ valid = tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPAck , 0 )
515+ require .True (t , valid )
516+
517+ // Connection should be tombstoned
518+ conn := tracker .connections [key ]
519+ require .NotNil (t , conn , "old connection should still be in map" )
520+ require .True (t , conn .IsTombstone (), "old connection should be tombstoned" )
521+
522+ // Now reuse the same port for a new connection
523+ tracker .TrackOutbound (srcIP , dstIP , srcPort , dstPort , TCPSyn , 100 )
524+
525+ // The old tombstoned entry should be replaced with a new one
526+ newConn := tracker .connections [key ]
527+ require .NotNil (t , newConn , "new connection should exist" )
528+ require .False (t , newConn .IsTombstone (), "new connection should not be tombstoned" )
529+ require .Equal (t , TCPStateSynSent , newConn .GetState ())
530+
531+ // SYN-ACK for the new connection should be valid
532+ valid = tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPSyn | TCPAck , 100 )
533+ require .True (t , valid , "SYN-ACK for new connection on reused port should be accepted" )
534+ require .Equal (t , TCPStateEstablished , newConn .GetState ())
535+
536+ // Data transfer should work
537+ tracker .TrackOutbound (srcIP , dstIP , srcPort , dstPort , TCPAck , 100 )
538+ valid = tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPPush | TCPAck , 500 )
539+ require .True (t , valid , "data should be allowed on new connection" )
540+ })
541+
542+ t .Run ("Outbound port reuse after RST" , func (t * testing.T ) {
543+ tracker := NewTCPTracker (DefaultTCPTimeout , logger , flowLogger )
544+ defer tracker .Close ()
545+
546+ key := ConnKey {SrcIP : srcIP , DstIP : dstIP , SrcPort : srcPort , DstPort : dstPort }
547+
548+ // Establish and RST a connection
549+ establishConnection (t , tracker , srcIP , dstIP , srcPort , dstPort )
550+ valid := tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPRst | TCPAck , 0 )
551+ require .True (t , valid )
552+
553+ conn := tracker .connections [key ]
554+ require .True (t , conn .IsTombstone (), "RST connection should be tombstoned" )
555+
556+ // Reuse the same port
557+ tracker .TrackOutbound (srcIP , dstIP , srcPort , dstPort , TCPSyn , 100 )
558+
559+ newConn := tracker .connections [key ]
560+ require .NotNil (t , newConn )
561+ require .False (t , newConn .IsTombstone ())
562+ require .Equal (t , TCPStateSynSent , newConn .GetState ())
563+
564+ valid = tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPSyn | TCPAck , 100 )
565+ require .True (t , valid , "SYN-ACK should be accepted after RST tombstone" )
566+ })
567+
568+ t .Run ("Inbound port reuse after close" , func (t * testing.T ) {
569+ tracker := NewTCPTracker (DefaultTCPTimeout , logger , flowLogger )
570+ defer tracker .Close ()
571+
572+ clientIP := srcIP
573+ serverIP := dstIP
574+ clientPort := srcPort
575+ serverPort := dstPort
576+ key := ConnKey {SrcIP : clientIP , DstIP : serverIP , SrcPort : clientPort , DstPort : serverPort }
577+
578+ // Inbound connection: client SYN → server SYN-ACK → client ACK
579+ tracker .TrackInbound (clientIP , serverIP , clientPort , serverPort , TCPSyn , nil , 100 , 0 )
580+ tracker .TrackOutbound (serverIP , clientIP , serverPort , clientPort , TCPSyn | TCPAck , 100 )
581+ tracker .TrackInbound (clientIP , serverIP , clientPort , serverPort , TCPAck , nil , 100 , 0 )
582+
583+ conn := tracker .connections [key ]
584+ require .Equal (t , TCPStateEstablished , conn .GetState ())
585+
586+ // Server-initiated close to reach Closed/tombstoned:
587+ // Server FIN (opposite dir) → CloseWait
588+ tracker .TrackOutbound (serverIP , clientIP , serverPort , clientPort , TCPFin | TCPAck , 100 )
589+ require .Equal (t , TCPStateCloseWait , conn .GetState ())
590+ // Client FIN-ACK (same dir as conn) → LastAck
591+ tracker .TrackInbound (clientIP , serverIP , clientPort , serverPort , TCPFin | TCPAck , nil , 100 , 0 )
592+ require .Equal (t , TCPStateLastAck , conn .GetState ())
593+ // Server final ACK (opposite dir) → Closed → tombstoned
594+ tracker .TrackOutbound (serverIP , clientIP , serverPort , clientPort , TCPAck , 100 )
595+
596+ require .True (t , conn .IsTombstone ())
597+
598+ // New inbound connection on same ports
599+ tracker .TrackInbound (clientIP , serverIP , clientPort , serverPort , TCPSyn , nil , 100 , 0 )
600+
601+ newConn := tracker .connections [key ]
602+ require .NotNil (t , newConn )
603+ require .False (t , newConn .IsTombstone ())
604+ require .Equal (t , TCPStateSynReceived , newConn .GetState ())
605+
606+ // Complete handshake: server SYN-ACK, then client ACK
607+ tracker .TrackOutbound (serverIP , clientIP , serverPort , clientPort , TCPSyn | TCPAck , 100 )
608+ tracker .TrackInbound (clientIP , serverIP , clientPort , serverPort , TCPAck , nil , 100 , 0 )
609+ require .Equal (t , TCPStateEstablished , newConn .GetState ())
610+ })
611+
612+ t .Run ("Late ACK on tombstoned connection is harmless" , func (t * testing.T ) {
613+ tracker := NewTCPTracker (DefaultTCPTimeout , logger , flowLogger )
614+ defer tracker .Close ()
615+
616+ key := ConnKey {SrcIP : srcIP , DstIP : dstIP , SrcPort : srcPort , DstPort : dstPort }
617+
618+ // Establish and close via passive close (server-initiated FIN → Closed → tombstoned)
619+ establishConnection (t , tracker , srcIP , dstIP , srcPort , dstPort )
620+ tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPFin | TCPAck , 0 ) // CloseWait
621+ tracker .TrackOutbound (srcIP , dstIP , srcPort , dstPort , TCPFin | TCPAck , 0 ) // LastAck
622+ tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPAck , 0 ) // Closed
623+
624+ conn := tracker .connections [key ]
625+ require .True (t , conn .IsTombstone ())
626+
627+ // Late ACK should be rejected (tombstoned)
628+ valid := tracker .IsValidInbound (dstIP , srcIP , dstPort , srcPort , TCPAck , 0 )
629+ require .False (t , valid , "late ACK on tombstoned connection should be rejected" )
630+
631+ // Late outbound ACK should not create a new connection (not a SYN)
632+ tracker .TrackOutbound (srcIP , dstIP , srcPort , dstPort , TCPAck , 0 )
633+ require .True (t , tracker .connections [key ].IsTombstone (), "late outbound ACK should not replace tombstoned entry" )
634+ })
635+ }
636+
488637func TestTCPTimeoutHandling (t * testing.T ) {
489638 // Create tracker with a very short timeout for testing
490639 shortTimeout := 100 * time .Millisecond
0 commit comments