1818//! closed-port blackhole (which only surfaces propagating errors) would miss.
1919//! To close the timing race inherent in a detached accept loop, the count is
2020//! read only after a bounded drain: the test opens a flush connection and
21- //! blocks until the accept thread has acknowledged (counted) it. Because
22- //! `accept()` hands out connections FIFO, every leak connection that completed
23- //! its TCP handshake before the flush is counted before the flush — the
24- //! assertion therefore covers all egress in flight when `obtain_proof`
21+ //! blocks until the accept thread has accepted *that specific connection*
22+ //! (tagged by its source port), not merely until any next counter tick.
23+ //! Because `accept()` hands out connections FIFO, every leak connection that
24+ //! completed its TCP handshake before the flush is counted before the flush —
25+ //! the assertion therefore covers all egress in flight when `obtain_proof`
2526//! returned. A leak that starts its handshake after the drain is outside the
2627//! window; that is the irreducible limit of observing a detached accept loop,
2728//! and it is the remaining, well-defined gap this guard accepts.
@@ -50,38 +51,45 @@ mod support;
5051/// A TCP listener that counts every accepted connection.
5152///
5253/// Acts as the proxy target: any leaked connection to a non-loopback
53- /// destination will be routed through this listener and increment the
54- /// counter. The count is guarded by a Mutex + Condvar so the test can block
55- /// until the detached accept thread has acknowledged a specific connection
56- /// (see [`ProxyObserver::drain`]) .
54+ /// destination will be routed through this listener and counted. Every
55+ /// accepted connection is recorded by its source port so the test's own
56+ /// flush connection ([`ProxyObserver::drain`]) can be recognised and
57+ /// excluded from the leak count .
5758struct ProxyObserver {
5859 _listener : Arc < TcpListener > ,
5960 port : u16 ,
60- count : Arc < Mutex < usize > > ,
61+ /// Source ports of every accepted connection, in accept order.
62+ accepted : Arc < Mutex < Vec < u16 > > > ,
63+ /// Source ports of the test's own flush connections (never counted).
64+ flush_ports : Arc < Mutex < Vec < u16 > > > ,
65+ /// Signalled after each accept so `drain` can wait on the flush.
6166 ack : Arc < Condvar > ,
6267}
6368
6469impl ProxyObserver {
6570 fn bind ( ) -> Self {
6671 let listener = Arc :: new ( TcpListener :: bind ( "127.0.0.1:0" ) . expect ( "bind proxy observer" ) ) ;
6772 let port = listener. local_addr ( ) . unwrap ( ) . port ( ) ;
68- let count = Arc :: new ( Mutex :: new ( 0usize ) ) ;
73+ let accepted = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
74+ let flush_ports = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
6975 let ack = Arc :: new ( Condvar :: new ( ) ) ;
70- let c = Arc :: clone ( & count ) ;
71- let a = Arc :: clone ( & ack) ;
76+ let a = Arc :: clone ( & accepted ) ;
77+ let c = Arc :: clone ( & ack) ;
7278 let l = Arc :: clone ( & listener) ;
7379 std:: thread:: spawn ( move || {
7480 for stream in l. incoming ( ) {
75- if stream. is_ok ( ) {
76- * c. lock ( ) . unwrap ( ) += 1 ;
77- a. notify_all ( ) ;
78- }
81+ let Ok ( stream) = stream else { continue } ;
82+ a. lock ( )
83+ . unwrap ( )
84+ . push ( stream. peer_addr ( ) . map ( |a| a. port ( ) ) . unwrap_or ( 0 ) ) ;
85+ c. notify_all ( ) ;
7986 }
8087 } ) ;
8188 ProxyObserver {
8289 _listener : listener,
8390 port,
84- count,
91+ accepted,
92+ flush_ports,
8593 ack,
8694 }
8795 }
@@ -90,29 +98,39 @@ impl ProxyObserver {
9098 self . port
9199 }
92100
101+ /// Number of accepted connections that are not the test's own flush
102+ /// connections — i.e. the leak count.
93103 fn count ( & self ) -> usize {
94- * self . count . lock ( ) . unwrap ( )
104+ let flush = self . flush_ports . lock ( ) . unwrap ( ) ;
105+ let accepted = self . accepted . lock ( ) . unwrap ( ) ;
106+ accepted. iter ( ) . filter ( |p| !flush. contains ( p) ) . count ( )
95107 }
96108
97109 /// Bounded acknowledgement barrier for the accept loop.
98110 ///
99- /// Opens a flush connection to the listener and blocks until the accept
100- /// thread has counted it (bounded by a timeout). Because `accept()`
101- /// hands out connections FIFO, once this flush is counted every
102- /// connection that completed its handshake before it is counted too.
103- /// This lets the test assert "no egress was accepted before this point"
104- /// without racing the detached accept loop.
111+ /// Opens a flush connection and blocks until the accept thread has
112+ /// accepted *that connection* (identified by its source port), bounded by
113+ /// a timeout. Because `accept()` hands out connections FIFO, once the
114+ /// flush is accepted every connection that completed its handshake before
115+ /// it has been accepted and recorded too — so counting on afterwards
116+ /// cannot miss a leak that was already in flight. The flush connection
117+ /// itself is never counted.
118+ ///
119+ /// A counter-tick barrier would be unsound here: sampling "current + 1"
120+ /// and waiting for any increment lets a leak accepted after the sample
121+ /// satisfy the wait, and the flush is then miscounted as that leak.
105122 fn drain ( & self ) {
106- let target = * self . count . lock ( ) . unwrap ( ) + 1 ;
107- let _flush =
123+ let flush =
108124 TcpStream :: connect ( ( "127.0.0.1" , self . port ) ) . expect ( "observer flush connection" ) ;
109- let mut count = self . count . lock ( ) . unwrap ( ) ;
110- while * count < target {
125+ let flush_port = flush. local_addr ( ) . unwrap ( ) . port ( ) ;
126+ self . flush_ports . lock ( ) . unwrap ( ) . push ( flush_port) ;
127+ let mut accepted = self . accepted . lock ( ) . unwrap ( ) ;
128+ while !accepted. contains ( & flush_port) {
111129 let ( guard, timed_out) = self
112130 . ack
113- . wait_timeout ( count , std:: time:: Duration :: from_secs ( 5 ) )
131+ . wait_timeout ( accepted , std:: time:: Duration :: from_secs ( 5 ) )
114132 . unwrap ( ) ;
115- count = guard;
133+ accepted = guard;
116134 assert ! (
117135 !timed_out. timed_out( ) ,
118136 "proxy observer drain timed out; accept loop stalled"
@@ -165,14 +183,13 @@ fn obtain_proof_consumes_the_mocked_service_and_makes_no_live_call() {
165183 challenge. assert ( ) ;
166184 answer. assert ( ) ;
167185
168- // Drain the accept loop before asserting: a leak connection that was
169- // accepted (or sitting in the backlog) when obtain_proof returned is
170- // counted ahead of the flush, so the count after drain reflects all
171- // egress in flight during the call. The flush connection itself is
172- // accepted and counted too, so it is excluded from the leak accounting
173- // (exactly one connection).
186+ // Drain the accept loop before asserting: the flush connection is not
187+ // counted, and because accept() is FIFO every leak connection that
188+ // completed its handshake before the flush is recorded before the flush
189+ // is — so the count after drain reflects all egress in flight during the
190+ // call.
174191 observer. drain ( ) ;
175- let leak_count = observer. count ( ) - count_before - 1 ;
192+ let leak_count = observer. count ( ) - count_before;
176193
177194 assert_eq ! (
178195 leak_count, 0 ,
0 commit comments