Loading content...
The TCP three-way handshake is one of the most important sequences in network communication. During this brief but critical exchange, two endpoints negotiate parameters, synchronize sequence numbers, and establish the shared state necessary for reliable data transfer. The SYN_SENT and SYN_RECEIVED states represent the intermediate phases of this handshake—moments where the connection exists in a partially-formed, vulnerable state.
These transitional states are where connections are most susceptible to failures, timeouts, and attacks. A packet lost here means connection failure. A malicious actor flooding SYN packets exploits these states. Network latency during this phase directly impacts perceived application responsiveness. Understanding SYN_SENT and SYN_RECEIVED deeply means understanding not just the protocol mechanics, but the performance characteristics, failure modes, and security implications that arise during connection establishment.
This page explores both states comprehensively—examining the client's perspective (SYN_SENT), the server's perspective (SYN_RECEIVED), the unusual simultaneous open case, and the practical implications for real-world systems.
By the end of this page, you will understand: (1) How clients enter and exit SYN_SENT state, (2) How servers enter and exit SYN_RECEIVED state, (3) The timeout and retransmission mechanisms during handshake, (4) TCP simultaneous open and the four-way handshake, (5) Security considerations during the partially-open states, and (6) Practical debugging of connection establishment issues.
When a client application calls connect() on a TCP socket, it initiates an active open—actively reaching out to establish a connection with a remote server. The socket immediately transitions from CLOSED to SYN_SENT, and the kernel transmits a SYN segment to the target address.
Entry Condition:
What the SYN Contains:
The initial SYN segment carries crucial information for connection establishment:
| Field | Purpose | Example Value |
|---|---|---|
| Sequence Number | Client's Initial Sequence Number (ISN) | 1234567890 |
| SYN flag | Indicates synchronization request | Set (1) |
| ACK flag | Not acknowledging anything yet | Clear (0) |
| Window Size | Client's initial receive window | 65535 |
| MSS Option | Maximum Segment Size | 1460 bytes |
| Window Scale Option | Window size multiplier | 7 (×128) |
| SACK Permitted Option | Selective acknowledgments allowed | Present |
| Timestamps Option | For RTT measurement and PAWS | Present |
The Initial Sequence Number (ISN) selection is security-critical. Early TCP implementations used predictable ISNs (incrementing clocks), enabling devastating attacks:
The ISN Prediction Attack (Historical):
Modern systems use cryptographically random ISNs:
ISN = hash(source_ip, source_port, dest_ip, dest_port, secret_key, timestamp)
This makes prediction virtually impossible while still ensuring ISNs don't repeat during the Maximum Segment Lifetime (MSL).
Normal Success Path:
Timeout Path:
Reset Path:
Simultaneous Open Path (Rare):
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
/** * Understanding SYN_SENT behavior in practice * Demonstrates timeouts and connection failure scenarios */#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <stdio.h>#include <errno.h>#include <time.h>#include <unistd.h> int main() { int sockfd; struct sockaddr_in server_addr; time_t start, end; sockfd = socket(AF_INET, SOCK_STREAM, 0); // Connect to a non-existent or firewalled host // This will demonstrate SYN_SENT timeout behavior server_addr.sin_family = AF_INET; server_addr.sin_port = htons(12345); inet_pton(AF_INET, "192.0.2.1", &server_addr.sin_addr); // TEST-NET-1 printf("Attempting connection (socket enters SYN_SENT)...\n"); start = time(NULL); int result = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); end = time(NULL); if (result < 0) { printf("Connection failed after %ld seconds\n", end - start); switch (errno) { case ETIMEDOUT: printf("Error: ETIMEDOUT - SYN retries exhausted\n"); printf("(No SYN+ACK received; host may be unreachable)\n"); break; case ECONNREFUSED: printf("Error: ECONNREFUSED - Received RST\n"); printf("(Server not listening on that port)\n"); break; case ENETUNREACH: printf("Error: ENETUNREACH - Network unreachable\n"); printf("(No route to destination)\n"); break; case EHOSTUNREACH: printf("Error: EHOSTUNREACH - Host unreachable\n"); printf("(ICMP host unreachable received)\n"); break; default: perror("connect"); } } else { printf("Connection established in %ld seconds\n", end - start); } close(sockfd); return 0;}The default SYN retry behavior (6 retries with exponential backoff) can result in 127+ second timeouts. For latency-sensitive applications, reduce tcp_syn_retries or implement application-level timeouts using non-blocking sockets with select()/poll()/epoll(). Cloud environments often use aggressive retry settings (2-3 retries).
When a server in LISTEN state receives a valid SYN packet, it creates a new connection record (or request socket) and transitions that connection into the SYN_RECEIVED state. Note carefully: the listening socket itself stays in LISTEN—SYN_RECEIVED applies to the embryonic connection being established.
Entry Conditions:
| From State | Trigger | Action |
|---|---|---|
| LISTEN | Receive valid SYN | Create request_sock, send SYN+ACK |
| SYN_SENT | Receive SYN (no ACK) | Simultaneous open; send SYN+ACK |
What the SYN+ACK Contains:
The server's response acknowledges the client's SYN and sends its own:
| Field | Value | Purpose |
|---|---|---|
| Sequence Number | Server's ISN | Initiating server's sequence space |
| ACK Number | Client's ISN + 1 | Acknowledging client's SYN |
| SYN flag | Set | Synchronizing server's sequence |
| ACK flag | Set | Acknowledging client's SYN |
| MSS Option | Server's MSS | May differ from client's |
| Window Scale | Server's value | If client offered it |
| SACK Permitted | Present | If client offered it |
In modern implementations (Linux, Windows), the server doesn't create a full socket for each SYN. Instead, it creates a lightweight request socket (or connection request block):
// Simplified Linux request_sock structure
struct request_sock {
struct sock_common __req_common;
struct request_sock *dl_next; // Queue linkage
u32 rcv_isn; // Client's ISN
u32 snt_isn; // Our ISN
u16 mss; // Negotiated MSS
u8 num_retrans; // SYN+ACK retries
u8 syncookie:1; // Created via SYN cookie?
struct timer_list rsk_timer; // Retransmission timer
};
This design minimizes memory consumption for half-open connections, which is crucial during SYN floods. A full socket (struct sock_common, struct inet_sock, etc.) may consume 1-2 KB, while a request socket uses only ~100-200 bytes.
Time
│
Server receives │
SYN from client ▼
─────────────────────
│
Create request_sock│
Send SYN+ACK │
Start timer │────────────────────────┐
(Enter SYN_RCVD) │ │
│ │
│ Timer expires? │ Waiting for
│ Resend SYN+ACK │ client's ACK
├────────────────────────┤
│ Timer expires again? │
│ Resend SYN+ACK │
├────────────────────────┤
│ │
Receive ACK ─────▶ │ Validate ACK │
│ Create full socket │
│ Move to accept queue │
│ (Enter ESTABLISHED) │
─────────────────────
│
App calls accept() │
Returns new fd ▼
Normal Success Path:
Timeout Path:
Reset Path:
The server commits resources when creating a request_sock, but the client hasn't proven its legitimacy (the source IP could be spoofed). This asymmetry—server commits resources based on unauthenticated SYN—is the fundamental vulnerability exploited by SYN floods. SYN cookies address this by deferring resource allocation until the final ACK proves the client's legitimacy.
| Aspect | SYN Queue | Accept Queue |
|---|---|---|
| Contains | Connections in SYN_RECEIVED | Connections in ESTABLISHED |
| Data structure | Hash table or list | Queue (FIFO) |
| Entry size | ~100-200 bytes (request_sock) | ~1-2 KB (full socket) |
| Controlled by | tcp_max_syn_backlog sysctl | backlog parameter + somaxconn |
| Overflow action | Drop SYN or use SYN cookies | Drop completed connection |
| Attacker target | SYN flood attack | slowloris/accept exhaustion |
The three-way handshake operates over an unreliable network layer—packets can be lost, delayed, or corrupted. TCP handles this through retransmission with exponential backoff, but the behavior during handshake differs from established connection retransmission.
When a client sends a SYN and receives no SYN+ACK:
Attempt │ Delay │ Cumulative
─────────┼───────────┼─────────────
1 │ 0s │ 0s (initial SYN)
2 │ 1s │ 1s (1st retry)
3 │ 2s │ 3s (2nd retry)
4 │ 4s │ 7s (3rd retry)
5 │ 8s │ 15s (4th retry)
6 │ 16s │ 31s (5th retry)
7 │ 32s │ 63s (6th retry)
Fail │ 64s │ 127s (give up)
Controlled by: net.ipv4.tcp_syn_retries (default: 6)
Total timeout ≈ 127 seconds before the connect() call returns ETIMEDOUT.
When a server sends SYN+ACK and receives no ACK:
Attempt │ Delay │ Cumulative
─────────┼───────────┼─────────────
1 │ 0s │ 0s (initial SYN+ACK)
2 │ 1s │ 1s (1st retry)
3 │ 2s │ 3s (2nd retry)
4 │ 4s │ 7s (3rd retry)
5 │ 8s │ 15s (4th retry)
6 │ 16s │ 31s (5th retry)
Drop │ │ ~63s (destroy request_sock)
Controlled by: net.ipv4.tcp_synack_retries (default: 5)
The request_sock is destroyed after approximately 63 seconds of no response.
1234567891011121314151617181920212223242526
#!/bin/bash# Tuning TCP handshake retransmission parameters # View current settingsecho "Current SYN retry settings:"sysctl net.ipv4.tcp_syn_retriessysctl net.ipv4.tcp_synack_retries # For faster failure detection (client side)# Reduce SYN retries from default 6 to 2# Total timeout: ~7 seconds (1 + 2 + 4)sudo sysctl -w net.ipv4.tcp_syn_retries=2 # For faster cleanup of dead connections (server side)# Reduce SYN+ACK retries from default 5 to 2# Dead connections cleaned up in ~7 secondssudo sysctl -w net.ipv4.tcp_synack_retries=2 # ⚠️ Warning: Lower values mean faster failure detection# but also less tolerance for packet loss and latency.# For high-latency networks (satellite, intercontinental),# keep defaults or even increase. # Make changes persistentecho "net.ipv4.tcp_syn_retries = 2" >> /etc/sysctl.d/99-tcp-tuning.confecho "net.ipv4.tcp_synack_retries = 2" >> /etc/sysctl.d/99-tcp-tuning.confThe exponential backoff pattern (1s, 2s, 4s, 8s, ...) serves multiple purposes:
Congestion Control: If packets are being lost due to network congestion, aggressive retransmission would worsen the congestion. Backing off gives the network time to recover.
Resource Conservation: Constant retransmission wastes CPU and bandwidth on both endpoints.
Diverse Network Conditions: The increasing delays accommodate networks with vastly different characteristics—from local networks (ms latency) to satellite links (600ms+ RTT).
Attack Mitigation: Rapid retransmission would amplify DDoS attack traffic.
| Lost Packet | Effect | Recovery |
|---|---|---|
| Initial SYN | Client retransmits | 1s delay minimum |
| SYN+ACK | Server retransmits | 1s delay minimum |
| Final ACK | Server retransmits SYN+ACK | 1s delay; client re-ACKs |
| Initial SYN (to wrong port) | Immediate RST | No delay (ECONNREFUSED) |
| Initial SYN (firewalled) | No response | Full timeout (127s default) |
Applications may implement multiple timeout layers:
Layer │ Typical Value │ Control
────────────────┼──────────────────┼─────────────────────
Kernel SYN │ 127 seconds │ tcp_syn_retries
Socket timeout │ 30 seconds │ SO_SNDTIMEO / alarm()
Application │ 5-10 seconds │ Application logic
User interface │ 3-5 seconds │ UX design
Best practice: Set application-level timeouts shorter than kernel timeouts to maintain responsiveness.
For sub-second connection timeouts, use non-blocking sockets: (1) Set socket non-blocking, (2) Call connect() (returns immediately with EINPROGRESS), (3) Use select()/poll()/epoll() with your desired timeout, (4) Check socket error status with getsockopt(SO_ERROR). This bypasses kernel SYN retry behavior.
TCP's state machine includes a rarely-encountered but fascinating scenario: simultaneous open, where both endpoints initiate connection at approximately the same time. Neither endpoint is purely a client or server—both perform active opens.
For simultaneous open to occur, the following must happen:
Host A Host B
│ │
│ connect() called │ connect() called
│ Send SYN (seq=100) │ Send SYN (seq=300)
│═════════════════════════════▶ │
│ ◀═════════════════════════════│
│ │
SYN crosses SYN in transit │
│ │
Receive SYN Receive SYN │
(no ACK bit) (no ACK bit) │
│ │
Enter SYN_RECEIVED │ Enter SYN_RECEIVED
Send SYN+ACK Send SYN+ACK │
│═════════════════════════════▶ │
│ ◀═════════════════════════════│
│ │
Receive SYN+ACK Receive SYN+ACK │
Enter ESTABLISHED Enter ESTABLISHED │
In simultaneous open, both endpoints follow this path:
CLOSED → SYN_SENT → SYN_RECEIVED → ESTABLISHED
This is unlike normal connection where:
The key insight: when a socket in SYN_SENT receives a SYN (without ACK), it knows a simultaneous open is occurring and transitions to SYN_RECEIVED rather than ESTABLISHED.
Simultaneous open is rare in practice because:
Asymmetric design: Most applications use client-server model where one side listens and one connects.
Timing requirements: Both SYNs must be in flight simultaneously, which requires very precise timing or high latency.
Known ports: Both sides must know each other's port in advance to connect().
However, simultaneous open can occur in:
TCP was designed to handle any combination of opens and closes. The state machine's completeness ensures that even unusual sequences work correctly. RFC 793 explicitly specifies simultaneous open behavior, and all conforming implementations must support it.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
"""Demonstrating TCP simultaneous openRequires precise timing and works best on local machine Run two instances of this script with opposite port assignments: Instance 1: MY_PORT=50000, PEER_PORT=50001 Instance 2: MY_PORT=50001, PEER_PORT=50000 Both will connect() to each other simultaneously.""" import socketimport timeimport threading MY_PORT = 50000PEER_PORT = 50001PEER_IP = "127.0.0.1" def attempt_simultaneous_open(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Must bind to known port so peer can connect to us sock.bind(('', MY_PORT)) # Wait for synchronization (in real scenario, use external sync) time.sleep(1) print(f"Attempting simultaneous open to {PEER_IP}:{PEER_PORT}") # Note: Normal connect() on bound socket # If peer also Connect()s at same time, simultaneous open occurs try: sock.connect((PEER_IP, PEER_PORT)) print("Connection established via simultaneous open!") # Verify we're connected local = sock.getsockname() remote = sock.getpeername() print(f"Local: {local}, Remote: {remote}") except socket.timeout: print("Connection timed out (peer may not have connected)") except Exception as e: print(f"Error: {e}") finally: sock.close() if __name__ == "__main__": attempt_simultaneous_open()Simultaneous open was more relevant in early ARPAnet days when the network was smaller and peer relationships were common. Modern networks are highly asymmetric, making this a curiosity rather than a common occurrence. However, understanding it illuminates the completeness of TCP's design.
The SYN_SENT and SYN_RECEIVED states represent a window of vulnerability. The connection exists but is not yet validated—creating opportunities for attacks, reconnaissance, and denial of service.
1. SYN Flood Attack
Target: SYN_RECEIVED state on servers
Mitigations:
2. ACK Flood Attack
Target: Processing overhead for spurious ACKs
Mitigations:
3. Port Scanning / Reconnaissance
Target: Discovery of listening services
Mitigations:
4. RST Injection Attack
Target: Disrupting connections during establishment
Mitigations:
| Attack | Target State | Goal | Defense |
|---|---|---|---|
| SYN Flood | SYN_RECEIVED | Exhaust server resources | SYN cookies, queue tuning |
| ACK Flood | N/A (fake ACKs) | CPU exhaustion | SYN cookies, rate limiting |
| Port Scan | LISTEN | Reconnaissance | Firewalls, IDS |
| RST Injection | SYN_SENT/SYN_RECEIVED | Connection termination | Sequence randomization, timestamps |
| ISN Prediction | SYN_SENT | Session hijacking | Cryptographic ISN |
Monitoring tools can identify attacks and anomalies:
# Check for SYN flood indicators
netstat -s | grep -i syn
# Example output during attack:
# 23451 SYNs to LISTEN sockets dropped
# 18234 times the listen queue of a socket overflowed
# 521432 SYN cookies sent
# 512891 SYN cookies received
# Monitor in real-time
watch -n1 'ss -tn state syn-recv | wc -l'
# Alert if SYN_RECEIVED count exceeds threshold
SYN_RECV_COUNT=$(ss -tn state syn-recv | wc -l)
if [ $SYN_RECV_COUNT -gt 1000 ]; then
echo "ALERT: High SYN_RECEIVED count ($SYN_RECV_COUNT)"
fi
# iptables rules for SYN flood protection
# Limit new connections per source IP
iptables -A INPUT -p tcp --syn -m connlimit \
--connlimit-above 100 --connlimit-mask 32 \
-j DROP
# Rate limit SYN packets overall
iptables -A INPUT -p tcp --syn -m limit \
--limit 100/s --limit-burst 200 \
-j ACCEPT
# Drop invalid packets (bad flags, wrong sequence)
iptables -A INPUT -m state --state INVALID -j DROP
# Log potential scans
iptables -A INPUT -p tcp --syn -m recent --name portscan \
--update --seconds 60 --hitcount 10 -j LOG \
--log-prefix "PORT SCAN DETECTED: "
No single defense is sufficient against handshake attacks. Modern protection combines multiple layers: network-level filtering, OS kernel hardening (SYN cookies, queue tuning), application-level rate limiting, and cloud-based DDoS mitigation. The handshake is inherently vulnerable by design, so mitigation is about raising the cost of attack rather than eliminating it.
Connection establishment problems manifest in various ways: timeouts, refused connections, slow startups. Understanding the handshake states helps pinpoint the cause.
Step 1: Identify the Symptom
| Symptom | Likely Cause | Check |
|---|---|---|
| ECONNREFUSED | RST received | Server not listening |
| ETIMEDOUT | No SYN+ACK | Firewall, routing, server down |
| Slow connect | Handshake completing slowly | Latency, packet loss, misconfiguration |
| Intermittent failure | Overloaded queues | Server queue depths |
Step 2: Verify Server is Listening
# On the server
ss -tlnp | grep PORT
lsof -i :PORT
# Expected: LISTEN state socket with correct program
Step 3: Check Network Path
# Can we reach the server?
ping server_ip
# Can we reach the port?
nmap -p PORT server_ip
# Or without nmap:
nc -zv server_ip PORT
Step 4: Capture the Handshake
# Capture TCP handshake packets
tcpdump -i any host server_ip and port PORT -nn
# Expected successful handshake:
# 1. Client → Server: SYN
# 2. Server → Client: SYN+ACK
# 3. Client → Server: ACK
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
#!/bin/bash# Comprehensive TCP connection diagnostic script if [ $# -ne 2 ]; then echo "Usage: $0 <host> <port>" exit 1fi HOST=$1PORT=$2 echo "=== TCP Connection Diagnostics ==="echo "Target: $HOST:$PORT"echo "Time: $(date)"echo "" # Test 1: DNS resolutionecho "--- DNS Resolution ---"if host $HOST > /dev/null 2>&1; then echo "✓ DNS resolves: $(host $HOST | head -1)"else echo "✗ DNS resolution failed"fiecho "" # Test 2: ICMP connectivityecho "--- ICMP Connectivity ---"if ping -c 1 -W 2 $HOST > /dev/null 2>&1; then echo "✓ Host responds to ping"else echo "✗ No ping response (may be firewalled)"fiecho "" # Test 3: TCP port reachabilityecho "--- TCP Port Check ---"timeout 5 bash -c "echo > /dev/tcp/$HOST/$PORT" 2>/dev/nullif [ $? -eq 0 ]; then echo "✓ Port $PORT is reachable"else echo "✗ Port $PORT is not reachable"fiecho "" # Test 4: Connection timingecho "--- Connection Timing ---"START=$(date +%s.%N)nc -zvw 10 $HOST $PORT 2>&1END=$(date +%s.%N)DURATION=$(echo "$END - $START" | bc)echo "Connection attempt took: ${DURATION}s"echo "" # Test 5: Capture analysis hintecho "--- For deeper analysis, run: ---"echo "sudo tcpdump -i any host $HOST and port $PORT -nn -c 10"echo ""echo "--- Expected packets: ---"echo " Client → Server: Flags [S] (SYN)"echo " Server → Client: Flags [S.] (SYN+ACK)"echo " Client → Server: Flags [.] (ACK)"Issue: Connection Refused Immediately
connect(): Connection refused (ECONNREFUSED)
ss -tlnp | grep PORT shows nothingIssue: Connection Timeout (Long Delay)
connect(): Connection timed out (ETIMEDOUT)
tcpdump shows SYN sent, no SYN+ACKIssue: Slow Connection (Several Seconds)
Cause A: Packet loss requiring retransmission
Cause B: High latency path
Cause C: DNS delay before connect()
Issue: Sporadic Connection Failures
Cause A: Server queue overloaded
Cause B: Intermittent network issues
When troubleshooting connection issues, tcpdump is your best friend. Capture on both endpoints if possible. A missing SYN+ACK reveals a server-side or network-side issue. A missing final ACK reveals a client-side issue. Seeing RST reveals a refusal. Seeing nothing reveals a firewall or routing problem.
We've explored the two transitional states of TCP connection establishment—the critical moments where connections are formed but not yet validated.
Once the three-way handshake completes successfully, the connection enters the ESTABLISHED state—the stable, data-transfer phase that most developers think of when they think of TCP connections. In the next page, we'll explore what ESTABLISHED means, how data transfer occurs, and the mechanisms that maintain connection health during this phase.
You now understand the SYN_SENT and SYN_RECEIVED states in depth—from the socket API operations that trigger them, through the kernel mechanics of retransmission, to the security implications of these vulnerable transitional states. This knowledge is essential for debugging connection establishment issues and understanding TCP's reliability guarantees.