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