CrackedRuby CrackedRuby

Overview

The TCP three-way handshake establishes a reliable connection between two network endpoints before data transmission begins. This connection establishment protocol ensures both client and server agree on initial sequence numbers, window sizes, and other TCP parameters necessary for reliable data transfer.

The handshake consists of three packet exchanges: SYN (synchronize), SYN-ACK (synchronize-acknowledge), and ACK (acknowledge). The client initiates the connection by sending a SYN packet with an initial sequence number. The server responds with a SYN-ACK packet containing its own sequence number and acknowledging the client's sequence number. The client completes the handshake by sending an ACK packet acknowledging the server's sequence number.

This process occurs beneath application-layer protocols like HTTP, SMTP, and FTP. When a web browser requests a page, the TCP three-way handshake completes before any HTTP data transfers. The handshake typically completes within milliseconds on local networks and hundreds of milliseconds across the internet, adding one round-trip time (RTT) to connection establishment.

Client                    Server
  |                         |
  |-------SYN seq=X-------->|  (Step 1: Client requests connection)
  |                         |
  |<----SYN-ACK seq=Y-------|  (Step 2: Server acknowledges and responds)
  |      ack=X+1            |
  |                         |
  |-------ACK seq=X+1-------|  (Step 3: Client acknowledges)
  |      ack=Y+1            |
  |                         |
  |   Connection Established|

Understanding the three-way handshake helps diagnose connection problems, optimize network performance, and implement custom network protocols. Network monitoring tools display handshake packets, making this knowledge essential for troubleshooting connection failures and analyzing network behavior.

Key Principles

The three-way handshake operates on several fundamental principles that ensure reliable connection establishment.

Sequence Number Synchronization

Each TCP connection uses sequence numbers to track bytes transmitted and received. During the handshake, both endpoints exchange their Initial Sequence Numbers (ISNs). The client selects a random ISN and transmits it in the SYN packet. The server generates its own random ISN and sends it in the SYN-ACK packet. This synchronization allows both sides to track data segments and detect missing or duplicate packets.

Modern implementations generate ISNs using cryptographic algorithms that incorporate timestamps and connection parameters. This prevents attackers from predicting sequence numbers and injecting malicious packets. The ISN increases over time, typically incrementing by a fixed amount every 4 microseconds, with additional randomization for security.

Connection State Management

Both client and server maintain connection state throughout the handshake. The client transitions from CLOSED to SYN-SENT after sending the initial SYN packet. The server moves from LISTEN to SYN-RECEIVED after receiving the SYN and sending SYN-ACK. When the client sends the final ACK, both endpoints enter the ESTABLISHED state and data transfer begins.

Client States:           Server States:
CLOSED                   LISTEN
  ↓ (send SYN)             ↓ (receive SYN)
SYN-SENT                 SYN-RECEIVED
  ↓ (receive SYN-ACK)      ↓ (receive ACK)
ESTABLISHED              ESTABLISHED

The state machine prevents invalid transitions and ensures proper cleanup if the handshake fails. If the client receives an unexpected packet while in SYN-SENT state, it discards the packet or sends a RST (reset) to terminate the connection attempt.

Acknowledgment Mechanism

Each packet in the handshake acknowledges previously received data. The acknowledgment number indicates the next sequence number the sender expects to receive. In the SYN-ACK packet, the server sets the acknowledgment number to the client's ISN plus one, confirming receipt of the SYN packet. The client's final ACK packet acknowledges the server's ISN plus one.

This bidirectional acknowledgment confirms both endpoints received the connection parameters. Without successful acknowledgment, the sender retransmits the packet after a timeout period. The retransmission timeout starts at a conservative value (typically 1-3 seconds) and increases exponentially with each failed attempt.

Window Size Negotiation

The handshake packets include a window size field that specifies how many bytes the sender can receive before requiring an acknowledgment. This flow control mechanism prevents fast senders from overwhelming slow receivers. The client advertises its receive window in the SYN packet, and the server advertises its window in the SYN-ACK packet.

Window scaling extends the window size beyond the 16-bit field limit. Both endpoints negotiate window scaling during the handshake using TCP options. If both sides support window scaling, they can advertise windows up to 1 gigabyte, improving throughput on high-bandwidth, high-latency networks.

TCP Options Exchange

The SYN and SYN-ACK packets carry TCP options that negotiate connection parameters. Maximum Segment Size (MSS) specifies the largest data segment the receiver accepts, typically derived from the Maximum Transmission Unit (MTU) minus IP and TCP headers. Selective Acknowledgment (SACK) support allows receivers to acknowledge non-contiguous data blocks. Timestamps enable round-trip time measurement and protect against wrapped sequence numbers.

Ruby Implementation

Ruby's Socket library provides direct access to TCP connections, exposing the three-way handshake through high-level abstractions. The TCPSocket and TCPServer classes handle handshake details automatically, while Socket provides lower-level control.

Basic TCP Client Connection

Creating a TCP client connection triggers the three-way handshake automatically. The socket blocks until the handshake completes or times out.

require 'socket'

# Initiates three-way handshake with remote server
socket = TCPSocket.new('example.com', 80)

# Connection now established, ready for data transfer
socket.puts "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
response = socket.read
socket.close

The TCPSocket.new method performs DNS resolution, creates a socket, sends the SYN packet, waits for SYN-ACK, sends ACK, and returns the connected socket. Network errors during the handshake raise Errno::ETIMEDOUT if the server doesn't respond, Errno::ECONNREFUSED if the server actively rejects the connection, or Errno::EHOSTUNREACH if routing fails.

TCP Server Implementation

A TCP server listens for incoming connections and accepts each connection through a complete three-way handshake.

require 'socket'

server = TCPServer.new('localhost', 8080)
puts "Server listening on port 8080"

# Accept blocks until client completes handshake
loop do
  client = server.accept
  
  # Three-way handshake complete, connection established
  puts "Client connected: #{client.peeraddr[3]}"
  
  client.puts "Hello from server"
  client.close
end

The accept method blocks in the SYN-RECEIVED state until receiving the final ACK packet. The returned client socket represents an ESTABLISHED connection ready for bidirectional communication.

Non-Blocking Connection

Non-blocking sockets return immediately from connection attempts, allowing applications to perform other work during the handshake.

require 'socket'

socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)

# Set non-blocking mode
socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)

remote_addr = Socket.sockaddr_in(80, 'example.com')

begin
  socket.connect(remote_addr)
rescue Errno::EINPROGRESS
  # Handshake in progress, socket in SYN-SENT state
  _, writable, _ = IO.select(nil, [socket], nil, 5)
  
  if writable
    # Check if connection succeeded
    begin
      socket.connect_nonblock(remote_addr)
    rescue Errno::EISCONN
      puts "Connection established"
    rescue => e
      puts "Connection failed: #{e.message}"
    end
  else
    puts "Connection timeout"
  end
end

socket.close

The EINPROGRESS error indicates the handshake started but hasn't completed. IO.select waits for the socket to become writable, signaling handshake completion. This pattern enables concurrent connection attempts and timeout control.

Connection Timeout Configuration

Ruby allows timeout configuration for connection establishment through the Timeout module or socket options.

require 'socket'
require 'timeout'

def connect_with_timeout(host, port, timeout_seconds)
  Timeout.timeout(timeout_seconds) do
    TCPSocket.new(host, port)
  end
rescue Timeout::Error
  raise "Connection to #{host}:#{port} timed out after #{timeout_seconds}s"
rescue Errno::ECONNREFUSED
  raise "Connection to #{host}:#{port} refused"
end

# Attempt connection with 5-second timeout
begin
  socket = connect_with_timeout('example.com', 80, 5)
  puts "Connected successfully"
  socket.close
rescue => e
  puts "Connection failed: #{e.message}"
end

The timeout covers the entire handshake process, including DNS resolution and packet transmission. Systems with slow DNS resolution may timeout before sending the initial SYN packet.

Socket Option Configuration

Ruby exposes TCP options set during the handshake through setsockopt before calling connect.

require 'socket'

socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)

# Configure TCP keepalive
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPIDLE, 60)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, 10)

# Set TCP_NODELAY to disable Nagle's algorithm
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)

# Configure send and receive buffer sizes
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 65536)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, 65536)

remote_addr = Socket.sockaddr_in(80, 'example.com')
socket.connect(remote_addr)

puts "Connected with custom TCP options"
socket.close

These options take effect during and after the handshake. Buffer size options advertise window sizes in the SYN packets, while TCP_NODELAY affects data transmission after connection establishment.

Practical Examples

Real-world scenarios demonstrate how the three-way handshake affects application behavior and performance.

HTTP Client with Connection Pooling

HTTP clients establish multiple TCP connections to improve throughput. Each connection requires a separate three-way handshake.

require 'socket'
require 'uri'

class SimpleHTTPClient
  def initialize(max_connections: 5)
    @max_connections = max_connections
    @connections = {}
  end
  
  def get(url)
    uri = URI(url)
    socket = get_connection(uri.host, uri.port || 80)
    
    request = "GET #{uri.path.empty? ? '/' : uri.path} HTTP/1.1\r\n"
    request += "Host: #{uri.host}\r\n"
    request += "Connection: keep-alive\r\n"
    request += "\r\n"
    
    socket.write(request)
    read_response(socket)
  end
  
  private
  
  def get_connection(host, port)
    key = "#{host}:#{port}"
    
    # Reuse existing connection if available
    if @connections[key] && !@connections[key].closed?
      return @connections[key]
    end
    
    # Perform three-way handshake for new connection
    puts "Establishing new connection to #{host}:#{port}"
    @connections[key] = TCPSocket.new(host, port)
  end
  
  def read_response(socket)
    response = ""
    while line = socket.gets
      response += line
      break if line == "\r\n"
    end
    response
  end
end

client = SimpleHTTPClient.new
# First request performs handshake
response1 = client.get('http://example.com/')
# Second request reuses connection, no handshake
response2 = client.get('http://example.com/page')

Connection reuse eliminates repeated handshakes, reducing latency by one RTT per request. Modern HTTP clients maintain connection pools across multiple hosts, balancing the overhead of maintaining idle connections against handshake costs.

Load Balancer Health Checks

Load balancers probe backend servers by establishing TCP connections and verifying successful handshakes.

require 'socket'
require 'timeout'

class HealthChecker
  def initialize(servers, check_interval: 10)
    @servers = servers
    @check_interval = check_interval
    @healthy_servers = []
  end
  
  def start
    Thread.new do
      loop do
        check_all_servers
        sleep @check_interval
      end
    end
  end
  
  def healthy_servers
    @healthy_servers.dup
  end
  
  private
  
  def check_all_servers
    results = @servers.map do |server|
      Thread.new { [server, check_server(server)] }
    end
    
    @healthy_servers = results.map(&:value)
                               .select { |_, healthy| healthy }
                               .map { |server, _| server }
    
    puts "Healthy servers: #{@healthy_servers.join(', ')}"
  end
  
  def check_server(server)
    host, port = server.split(':')
    
    Timeout.timeout(3) do
      socket = TCPSocket.new(host, port.to_i)
      socket.close
      true
    end
  rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Timeout::Error, SocketError
    false
  end
end

servers = ['localhost:8080', 'localhost:8081', 'localhost:8082']
checker = HealthChecker.new(servers, check_interval: 5)
checker.start

sleep 30  # Monitor for 30 seconds

Failed handshakes indicate server unavailability. The checker distinguishes between connection refused (server down), timeout (network issues), and successful connections (server healthy). This pattern forms the basis of load balancer health checking.

Parallel Connection Establishment

Applications often establish multiple connections concurrently to reduce overall latency.

require 'socket'

def parallel_connect(hosts_and_ports, timeout: 5)
  threads = hosts_and_ports.map do |host, port|
    Thread.new do
      begin
        start_time = Time.now
        socket = TCPSocket.new(host, port)
        duration = Time.now - start_time
        [host, port, :success, duration, socket]
      rescue => e
        duration = Time.now - start_time
        [host, port, :failed, duration, e]
      end
    end
  end
  
  # Wait for all handshakes to complete
  results = threads.map(&:value)
  
  results.each do |host, port, status, duration, result|
    if status == :success
      puts "Connected to #{host}:#{port} in #{(duration * 1000).round}ms"
      result.close
    else
      puts "Failed to connect to #{host}:#{port}: #{result.message}"
    end
  end
  
  results
end

servers = [
  ['example.com', 80],
  ['google.com', 80],
  ['github.com', 443]
]

parallel_connect(servers)

Parallel handshakes complete in the time of the slowest connection rather than the sum of all connection times. This technique improves startup latency for applications connecting to multiple services.

Retry Logic with Exponential Backoff

Network issues may cause handshake failures. Retry logic with exponential backoff prevents overwhelming failing servers.

require 'socket'

def connect_with_retry(host, port, max_attempts: 5)
  attempt = 1
  backoff = 1
  
  while attempt <= max_attempts
    begin
      puts "Connection attempt #{attempt} to #{host}:#{port}"
      socket = TCPSocket.new(host, port)
      puts "Connected successfully on attempt #{attempt}"
      return socket
      
    rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
      puts "Attempt #{attempt} failed: #{e.message}"
      
      if attempt == max_attempts
        raise "Failed to connect after #{max_attempts} attempts"
      end
      
      sleep backoff
      backoff *= 2  # Exponential backoff
      attempt += 1
    end
  end
end

begin
  socket = connect_with_retry('localhost', 8080)
  socket.puts "Hello"
  socket.close
rescue => e
  puts "Connection failed: #{e.message}"
end

Exponential backoff reduces load on failing servers and gives transient issues time to resolve. The backoff interval doubles after each failure: 1s, 2s, 4s, 8s, preventing retry storms that exacerbate server problems.

Security Implications

The three-way handshake creates several security vulnerabilities that attackers exploit to disrupt services or compromise systems.

SYN Flood Attacks

SYN flood attacks exploit the handshake process by sending numerous SYN packets without completing the handshake. The server allocates resources for each half-open connection (SYN-RECEIVED state), exhausting memory and preventing legitimate connections.

Attackers typically spoof source IP addresses in SYN packets, making the server send SYN-ACK responses to non-existent hosts. The server waits for ACK packets that never arrive, leaving connections in SYN-RECEIVED state until timeout.

Mitigation strategies include:

  • SYN Cookies: Generate ISNs cryptographically from connection parameters, eliminating state storage during handshake. The server validates returning ACK packets by recomputing the expected sequence number.
  • Connection Limits: Restrict half-open connections per source IP address, blocking excessive SYN packets from single sources.
  • Firewall Rules: Drop SYN packets matching attack patterns or rate-limit SYN packets per source.

Ruby applications behind reverse proxies inherit SYN flood protection from the proxy layer. Direct internet-facing Ruby servers require operating system tuning:

require 'socket'

server = TCPServer.new('0.0.0.0', 8080)

# Configure socket options to resist SYN floods
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_DEFER_ACCEPT, 5)

# Limit accept queue to prevent memory exhaustion
server.listen(128)  # Limit backlog of pending connections

loop do
  # Accept with timeout to prevent indefinite blocking
  begin
    if IO.select([server], nil, nil, 1)
      client = server.accept
      Thread.new(client) do |c|
        handle_client(c)
      end
    end
  rescue => e
    puts "Accept failed: #{e.message}"
  end
end

TCP_DEFER_ACCEPT delays accept until data arrives, reducing resource consumption for connection attempts that never send data.

Connection Hijacking

Attackers who predict sequence numbers can inject packets into established connections or hijack connections by sending spoofed ACK packets. Modern sequence number generation algorithms prevent prediction through cryptographic randomness.

Ruby applications cannot directly control ISN generation, relying on kernel implementation. Ensuring current kernel versions with RFC 6528 compliant sequence number generation protects against hijacking.

Reset Attacks

Attackers send RST (reset) packets with spoofed source addresses to terminate legitimate connections. Successful reset attacks require the attacker to guess the current sequence number within the receive window.

TCP implementations validate RST packets strictly, requiring sequence numbers within the current window. Window scaling increases the target range, making reset attacks easier. Applications sensitive to connection termination should detect unexpected resets and attempt reconnection:

require 'socket'

def resilient_connection(host, port)
  socket = nil
  
  loop do
    begin
      socket = TCPSocket.new(host, port)
      yield socket
      
    rescue Errno::ECONNRESET, Errno::EPIPE => e
      puts "Connection reset: #{e.message}, reconnecting..."
      socket.close if socket && !socket.closed?
      sleep 1
      
    rescue => e
      puts "Fatal error: #{e.message}"
      socket.close if socket && !socket.closed?
      raise
    end
  end
end

resilient_connection('example.com', 80) do |socket|
  socket.puts "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
  response = socket.read
  puts response
end

Amplification Attacks

Attackers spoof victim IP addresses in SYN packets sent to multiple servers, causing those servers to flood the victim with SYN-ACK responses. This amplifies attack traffic through legitimate servers.

Rate limiting and source validation at network boundaries prevent participation in amplification attacks. Ruby servers cannot directly prevent this attack pattern but should implement connection rate limiting:

require 'socket'

class RateLimitedServer
  def initialize(port, max_connections_per_ip: 10)
    @server = TCPServer.new('0.0.0.0', port)
    @max_connections_per_ip = max_connections_per_ip
    @connections = Hash.new(0)
  end
  
  def start
    loop do
      client = @server.accept
      client_ip = client.peeraddr[3]
      
      if @connections[client_ip] >= @max_connections_per_ip
        puts "Rate limit exceeded for #{client_ip}"
        client.close
        next
      end
      
      @connections[client_ip] += 1
      
      Thread.new(client, client_ip) do |c, ip|
        begin
          handle_client(c)
        ensure
          @connections[ip] -= 1
          c.close
        end
      end
    end
  end
  
  private
  
  def handle_client(client)
    # Process client request
  end
end

Common Pitfalls

Developers encounter recurring issues when working with TCP connections and the three-way handshake.

Ignoring Connection Timeouts

The default TCP connection timeout varies by operating system, often ranging from 20-120 seconds. Applications that don't configure timeouts block indefinitely on unresponsive servers.

# Problematic: blocks for system default timeout
socket = TCPSocket.new('unreachable-host.com', 80)

# Better: explicit timeout control
require 'timeout'

begin
  Timeout.timeout(5) do
    socket = TCPSocket.new('unreachable-host.com', 80)
  end
rescue Timeout::Error
  puts "Connection timeout after 5 seconds"
end

Always set explicit connection timeouts appropriate to application requirements. Interactive applications require short timeouts (1-5 seconds), while batch processes tolerate longer waits.

Resource Leaks from Failed Handshakes

Failed connection attempts leave sockets in half-open states, consuming file descriptors. Applications making many connection attempts must close sockets explicitly:

# Leaks socket on connection failure
def connect_unsafe(host, port)
  socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
  addr = Socket.sockaddr_in(port, host)
  socket.connect(addr)  # May raise exception
  socket
end

# Properly handles failures
def connect_safe(host, port)
  socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
  addr = Socket.sockaddr_in(port, host)
  
  begin
    socket.connect(addr)
    socket
  rescue
    socket.close
    raise
  end
end

Ruby's TCPSocket.new handles cleanup automatically, but lower-level Socket usage requires manual cleanup.

Firewall and NAT Traversal Issues

Firewalls block SYN packets or drop responses, causing handshake failures that applications misinterpret. NAT devices track connection state, rejecting packets that don't match existing mappings.

Connection attempts through corporate firewalls may require specific timeout values and retry logic. Testing in production environments reveals firewall behaviors not present in development:

def connect_through_firewall(host, port)
  attempt = 1
  errors = []
  
  3.times do
    begin
      puts "Attempt #{attempt}: connecting to #{host}:#{port}"
      socket = TCPSocket.new(host, port)
      puts "Connection succeeded"
      return socket
      
    rescue Errno::ETIMEDOUT => e
      errors << "Timeout on attempt #{attempt}"
      
    rescue Errno::ECONNREFUSED => e
      errors << "Connection refused on attempt #{attempt}"
      return nil  # Server explicitly rejected, don't retry
      
    rescue => e
      errors << "#{e.class}: #{e.message}"
    end
    
    attempt += 1
    sleep 2
  end
  
  puts "All attempts failed:"
  errors.each { |err| puts "  #{err}" }
  nil
end

Incomplete Error Handling

Different handshake failures produce different exceptions. Applications must handle each error type appropriately:

  • ECONNREFUSED: Server actively rejected connection (wrong port, service down)
  • ETIMEDOUT: No response received (firewall, routing failure, host down)
  • EHOSTUNREACH: No route to host (routing problem)
  • ENETUNREACH: Network unreachable (network interface down)
require 'socket'

def connect_with_diagnostics(host, port)
  TCPSocket.new(host, port)
  
rescue Errno::ECONNREFUSED
  puts "Connection refused - server not listening on port #{port}"
  puts "Check if service is running: lsof -i :#{port}"
  
rescue Errno::ETIMEDOUT
  puts "Connection timeout - no response from #{host}:#{port}"
  puts "Check firewall rules and network connectivity"
  
rescue Errno::EHOSTUNREACH
  puts "Host unreachable - no route to #{host}"
  puts "Check routing table: netstat -rn"
  
rescue Errno::ENETUNREACH
  puts "Network unreachable - network interface may be down"
  puts "Check network interfaces: ifconfig or ip addr"
  
rescue SocketError => e
  puts "DNS resolution failed: #{e.message}"
  puts "Check DNS configuration: nslookup #{host}"
  
rescue => e
  puts "Unexpected error: #{e.class} - #{e.message}"
end

Race Conditions in Non-Blocking Connections

Non-blocking connection attempts require careful state checking. Testing for connection completion without proper synchronization causes race conditions:

# Problematic: races between connect_nonblock and IO.select
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
addr = Socket.sockaddr_in(80, 'example.com')

begin
  socket.connect_nonblock(addr)
rescue Errno::EINPROGRESS
  # Race: socket might complete between exception and select
  IO.select(nil, [socket], nil, 5)
end

# Better: check socket state after select
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
addr = Socket.sockaddr_in(80, 'example.com')

begin
  socket.connect_nonblock(addr)
rescue Errno::EINPROGRESS
  _, writable, _ = IO.select(nil, [socket], nil, 5)
  
  if writable
    begin
      socket.connect_nonblock(addr)
    rescue Errno::EISCONN
      # Connection succeeded
    rescue => e
      puts "Connection failed: #{e.message}"
      socket.close
    end
  end
end

Misunderstanding Connection Establishment vs. Authentication

A successful handshake establishes transport-layer connectivity but doesn't validate application-layer credentials. SSL/TLS handshakes occur after TCP handshake completion:

require 'socket'
require 'openssl'

# TCP handshake completes here
socket = TCPSocket.new('example.com', 443)

# SSL/TLS handshake begins after TCP establishment
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)

begin
  # SSL handshake may fail even with successful TCP connection
  ssl_socket.connect
  puts "SSL handshake complete"
rescue OpenSSL::SSL::SSLError => e
  puts "SSL handshake failed: #{e.message}"
ensure
  ssl_socket.close if ssl_socket
end

Applications must distinguish between transport failures (TCP handshake) and authentication failures (SSL/TLS or application protocol).

Tools & Ecosystem

Several Ruby gems and system tools facilitate working with TCP connections and analyzing handshake behavior.

TCPSocket and Socket Libraries

Ruby's standard library provides TCPSocket for high-level connection management and Socket for low-level control. TCPSocket abstracts handshake details:

require 'socket'

# High-level: TCPSocket handles all handshake details
tcp = TCPSocket.new('example.com', 80)

# Low-level: Socket provides explicit control
sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
addr = Socket.sockaddr_in(80, 'example.com')
sock.connect(addr)

Socket enables advanced options like non-blocking mode and fine-grained timeout control unavailable in TCPSocket.

EventMachine and Async Networking

EventMachine provides an event-driven networking library built on the Reactor pattern, managing multiple connections concurrently:

require 'eventmachine'

module EchoClient
  def connection_completed
    puts "TCP handshake completed"
    send_data("Hello server\n")
  end
  
  def receive_data(data)
    puts "Received: #{data}"
    close_connection
  end
  
  def unbind
    puts "Connection closed"
    EventMachine.stop
  end
end

EventMachine.run do
  EventMachine.connect('example.com', 80, EchoClient)
end

EventMachine triggers the connection_completed callback after handshake completion, separating connection establishment from data handling.

Wireshark and Packet Analysis

Wireshark captures and displays TCP packets, making handshake sequences visible. Filtering by tcp.flags.syn shows SYN packets, tcp.flags.syn and tcp.flags.ack shows SYN-ACK packets:

# Wireshark display filter for handshake packets
tcp.flags.syn == 1

# tcpdump command-line equivalent
tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' -nn

Analyzing packet captures reveals handshake timing, retransmissions, and connection failures. Time deltas between SYN, SYN-ACK, and ACK indicate network latency.

netstat and ss Commands

The netstat and ss commands display socket states, including connections in SYN-SENT and SYN-RECEIVED states:

# Show all TCP sockets with numeric addresses
netstat -tan

# Show sockets in SYN-SENT state
ss -tan state syn-sent

# Show listening sockets with process information
ss -tlnp

# Count connections by state
netstat -tan | awk '{print $6}' | sort | uniq -c

Applications with high SYN-SENT counts may indicate connection problems. SYN-RECEIVED states accumulate during SYN flood attacks.

Net::Ping and Connection Testing

The net-ping gem provides connection testing functionality:

require 'net/ping'

# Test TCP connectivity by completing handshake
check = Net::Ping::TCP.new('example.com', 80, 5)

if check.ping?
  puts "Host is reachable, handshake completed"
  puts "Duration: #{check.duration}s"
else
  puts "Host unreachable: #{check.exception}"
end

Net::Ping attempts TCP handshakes without sending application data, measuring connectivity and latency.

Middleware and Reverse Proxies

Nginx, HAProxy, and other reverse proxies handle handshakes on behalf of Ruby applications. Connection pooling between proxy and application servers amortizes handshake costs:

# Unicorn configuration for socket reuse
worker_processes 4
listen 8080, backlog: 512, tcp_nopush: true, tcp_nodelay: true

before_fork do |server, worker|
  # Close database connections before forking
  defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect!
end

after_fork do |server, worker|
  # Reestablish database connections after fork
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

Proxies maintain persistent connections to upstream servers, converting many client handshakes into fewer backend handshakes.

Connection Pool Implementations

The connection_pool gem manages connection reuse, reducing handshake overhead:

require 'connection_pool'
require 'socket'

pool = ConnectionPool.new(size: 5, timeout: 5) do
  TCPSocket.new('example.com', 80)
end

# Checkout connection from pool (reuses existing connection)
pool.with do |socket|
  socket.puts "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
  response = socket.gets
  puts response
end

Connection pools maintain established connections, eliminating handshake latency for subsequent requests. Pool exhaustion causes new handshakes when no idle connections exist.

Reference

TCP Handshake Packet Structure

Packet Flags Sequence Number Acknowledgment Number Purpose
SYN SYN=1, ACK=0 Client ISN 0 Request connection
SYN-ACK SYN=1, ACK=1 Server ISN Client ISN + 1 Accept connection
ACK SYN=0, ACK=1 Client ISN + 1 Server ISN + 1 Confirm connection

Connection State Transitions

Current State Event Next State
CLOSED Application opens connection SYN-SENT
LISTEN Receive SYN SYN-RECEIVED
SYN-SENT Receive SYN-ACK ESTABLISHED
SYN-RECEIVED Receive ACK ESTABLISHED

Common Error Codes

Error Code Meaning Typical Cause
ECONNREFUSED Connection refused Server not listening on port
ETIMEDOUT Connection timeout No response from server
EHOSTUNREACH Host unreachable Routing failure
ENETUNREACH Network unreachable Network interface down
EINPROGRESS Operation in progress Non-blocking connect started
EISCONN Already connected Connect called on connected socket

Ruby Socket Methods for Handshake Control

Method Description Use Case
TCPSocket.new Create connected socket Simple client connections
Socket.connect Initiate connection Low-level control
Socket.connect_nonblock Non-blocking connect Async connection establishment
TCPServer.accept Accept incoming connection Server implementation
IO.select Wait for socket readiness Monitor handshake completion
Socket.setsockopt Configure socket options Tune handshake behavior

TCP Options Negotiated During Handshake

Option Purpose Impact
MSS Maximum segment size Determines packet fragmentation
Window Scale Extend window size range Improves high-bandwidth performance
SACK Selective acknowledgment Improves loss recovery
Timestamp RTT measurement Enables performance optimization
TCP Fast Open Data in SYN packet Reduces latency for repeat connections

Handshake Timing Metrics

Metric Typical Value Significance
Local handshake <1ms Minimal latency overhead
Regional handshake 10-50ms One regional RTT
Cross-continent 100-300ms Intercontinental latency
Satellite link 500-700ms Geostationary orbit delay
Retransmission timeout 1-3s initial Connection failure detection

Socket Options Affecting Handshake

Option Level Description
SO_REUSEADDR SOL_SOCKET Allow immediate port reuse
SO_KEEPALIVE SOL_SOCKET Enable connection keepalive
TCP_NODELAY IPPROTO_TCP Disable Nagle's algorithm
TCP_DEFER_ACCEPT IPPROTO_TCP Delay accept until data arrives
SO_SNDBUF SOL_SOCKET Set send buffer size
SO_RCVBUF SOL_SOCKET Set receive buffer size