This document describes the connection retry mechanism and strategy selection logic in Firestack's dialers subsystem. The retrier provides transparent retry capabilities with configurable splitting strategies to bypass network censorship and improve connection reliability. For the actual splitting implementations and desynchronization attacks, see 5.2. Splitting and Desync Attacks.
The retrier system in intra/dialers/ implements intelligent connection retry logic with multiple dial strategies. It transparently retries failed connections by:
tee buffer [intra/dialers/retrier.go:108].ippPins [intra/dialers/retrier.go:68].SigCond [intra/dialers/retrier.go:116].The system supports both single-dialer mode (with strategy progression) and multi-dialer mode (cycling through different dialers).
The retrier struct implements core.DuplexConn and core.RetrierConn interfaces, acting as a transparent wrapper around TCP connections with retry capabilities [intra/dialers/retrier.go:119-120].
Title: retrier Entity Relationship
Sources: intra/dialers/retrier.go73-117 intra/core/sigcond.go14-76
The retrier supports two dialing modes with different entry points:
| Entry Point | Mode | Purpose | Max Retries | Strategy Selection |
|---|---|---|---|---|
DialWithSplitRetry | Single-dialer | Strategy-based retries | maxRetryCount (3) | Based on DialerOpts.Retry + Strat |
DialAny | Multi-dialer | Dialer failover | len(dialers) | Fixed RetryWithSplit + SplitNever |
Title: retrier Initialization Flow
Sources: intra/dialers/retrier.go150-167 intra/dialers/retrier.go198-222 intra/dialers/retrier.go169-175
The settings.Retry constants control when retries occur and when splitting is applied [intra/dialers/retrier.go:248-312]:
Title: Retry Logic States
| Strategy | Split on Attempt 0? | Split on Attempt 1+? | Max Attempts | Use Case |
|---|---|---|---|---|
RetryNever | ✓ (if Strat set) | N/A | 1 | Fast-fail, no retries |
RetryWithSplit | ✗ | ✓ | maxRetries | Normal connection, split on retry |
RetryAfterSplit | ✓ | ✓ (until max) | maxRetries | Aggressive splitting upfront |
Sources: intra/dialers/retrier.go248-312 intra/dialers/retrier.go50-63
Split strategies determine how the initial TCP segment is fragmented. These are settings.Split* constants:
| Strategy | Description | Implementation | Auto Cycle Order |
|---|---|---|---|
SplitNever | No splitting | Direct protect.Dial() [intra/dialers/retrier.go:405] | N/A |
SplitTCP | Split TCP segment | splitter with TCP mode [intra/dialers/direct_split.go:64] | 1st (Auto) |
SplitTCPOrTLS | Split TLS record if port 443, else TCP | splitter with TLS mode [intra/dialers/direct_split.go:66] | 2nd (Auto) |
SplitDesync | TCB Desynchronization Attack | overwriteSplitter [intra/dialers/split_and_desync.go:45] | 3rd (Auto) |
SplitAuto | Cycle through strategies on retry | See Auto Mode Logic | Cycles 1→2→3 |
Sources: intra/dialers/retrier.go405-419 intra/dialers/direct_split.go61-73
The dialStratLocked() function implements the core strategy selection logic [intra/dialers/retrier.go:248]:
Title: dialStratLocked Logic
Sources: intra/dialers/retrier.go248-312
When Strat == SplitAuto, the retrier cycles through strategies based on attemptCycle = retryCount % maxRetries [intra/dialers/retrier.go:278]:
| Retry Strategy | attemptCycle=0 | attemptCycle=1 | attemptCycle=2 | attemptCycle=3+ |
|---|---|---|---|---|
RetryNever | SplitTCP | N/A | N/A | N/A |
RetryWithSplit | SplitDesync | SplitTCP | SplitTCPOrTLS | SplitDesync |
RetryAfterSplit | SplitTCP | SplitTCPOrTLS | SplitTCP (no split) | N/A |
Note: For RetryAfterSplit + SplitAuto, splitting stops at retryCount >= maxRetries, so the strategy doesn't matter for later cycles [intra/dialers/retrier.go:266-269].
Sources: intra/dialers/retrier.go278-306
The DialAny() function enables connection attempts across multiple dialers with intelligent prioritization [intra/dialers/retrier.go:198].
Title: Multi-Dialer Execution and Pinning
Sources: intra/dialers/retrier.go198-222 intra/dialers/retrier.go177-196 intra/dialers/retrier.go372-395 intra/dialers/retrier.go798-807
The ippPins global variable is a core.Sieve[netip.AddrPort, string] that maintains a limited-time mapping between IP:port destinations and dialer IDs [intra/dialers/retrier.go:68]. It uses a TTL of desync_cache_ttl (30 seconds) [intra/dialers/split_and_desync.go:35].
Title: ippPins Cache Lifecycle
Sources: intra/dialers/retrier.go68 intra/dialers/retrier.go177-196 intra/dialers/retrier.go798-807 intra/dialers/split_and_desync.go35
The retrier buffers the first write (typically the TLS ClientHello or HTTP request) in the tee field for replay on retry [intra/dialers/retrier.go:108].
Title: Tee Buffer Write Sequence
Sources: intra/dialers/retrier.go604-686 intra/dialers/retrier.go560-590
Title: Retry Replay Flow
Sources: intra/dialers/retrier.go473-558 intra/dialers/retrier.go425-461
The calcTimeout() function computes an adaptive timeout for the first read after a write, based on connection RTT [intra/dialers/retrier.go:134]:
timeout = max(rtt * 2, ciel) + min(2 * rtt, floor)
where:
ciel = max(1, (cielRetryReadTimeoutSec / spread)) * time.Second
floor = min(300, (floorRetryReadTimeoutMillis / spread)) * time.Millisecond
spread = max(1, spread) (distributes timeout budget)
| Constant | Value | Purpose |
|---|---|---|
cielRetryReadTimeoutSec | 9s | Maximum base timeout [intra/dialers/retrier.go:58] |
floorRetryReadTimeoutMillis | 1000ms | Minimum additional timeout [intra/dialers/retrier.go:60] |
Sources: intra/dialers/retrier.go134-142 intra/dialers/retrier.go50-63 intra/dialers/retrier.go324-361
The retryDone field is a core.SigCond that coordinates retry completion across concurrent readers and writers [intra/dialers/retrier.go:116].
Title: core.SigCond States
Sources: intra/core/sigcond.go14-76
The retrier delegates to splitter implementations when dialStrat != SplitNever:
| Strategy | Implementation | Created By |
|---|---|---|
SplitTCP, SplitTCPOrTLS | splitter | intra/dialers/retrier.go414-419 |
SplitDesync | overwriteSplitter | intra/dialers/retrier.go409 |
The retrier's conn field holds either:
protect.Conn (underlying connection)*splitter (when SplitTCP or SplitTCPOrTLS)*overwriteSplitter (when SplitDesync)Sources: intra/dialers/retrier.go372-420 intra/dialers/direct_split.go37-42 intra/dialers/split_and_desync.go45-55
The retrier handles several edge cases:
r.conn != nil before use [intra/dialers/retrier.go:474].readDone and writeDone atomic flags [intra/dialers/retrier.go:85-86].maxEmptyReads (3) empty reads are tolerated before returning io.ErrNoProgress [intra/dialers/retrier.go:52].minExpectedTLSRead (16 bytes) trigger retry with errTLSHandshake [intra/dialers/retrier.go:54].When all dialers fail in multi-dialer mode, the nextDialerIdx resets to 0 if retries remain [intra/dialers/retrier.go:375-379]:
Sources: intra/dialers/retrier.go375-379
| Entity | Type | File | Purpose |
|---|---|---|---|
retrier | struct | intra/dialers/retrier.go73 | Main retry orchestrator. |
DialWithSplitRetry() | func | intra/dialers/retrier.go150 | Single-dialer entry point. |
DialAny() | func | intra/dialers/retrier.go198 | Multi-dialer entry point. |
dialStratLocked() | method | intra/dialers/retrier.go248 | Strategy selection algorithm. |
retryWriteReadLocked() | method | intra/dialers/retrier.go425 | Retry dial + write + read. |
calcTimeout() | func | intra/dialers/retrier.go134 | Adaptive timeout calculation. |
reprioritize() | func | intra/dialers/retrier.go177 | Dialer reordering by cache. |
ippPins | var | intra/dialers/retrier.go68 | IP:port → dialer ID cache. |
SigCond | struct | intra/core/sigcond.go29 | Signalable boolean (Ref in retrier.go). |
Sources: intra/dialers/retrier.go intra/dialers/split_and_desync.go intra/dialers/direct_split.go
Refresh this wiki