Day 32 — TCP Fundamentals
Learning Objectives
- Build a TCP echo server and observe connection states
- Understand the 3-way handshake and why it matters for SYN floods
- Use
netstat/ssto observe socket states during development
Key Concepts
TCP 3-Way Handshake:
Client → SYN(seq=1000) → Server [SYN_SENT / SYN_RCVD]
Client ← SYN-ACK(seq=2000) ← Server
Client → ACK(seq=2001) → Server [ESTABLISHED]
SYN Flood Attack:
Attacker sends millions of SYNs without completing the handshake
Server allocates state for each SYN → half-open connection queue fills
Legitimate connections rejected → denial of service
Defense: SYN cookies (kernel), connection rate limiting (your code, Day 94)
Connection States after Close:
TIME_WAIT (2*MSL ≈ 60-120s) — normal, server-side after graceful close
CLOSE_WAIT — application bug: peer closed but you haven't called conn.Close()
Exercise: TCP Echo Server
package main
import (
"bufio"
"fmt"
"net"
"sync/atomic"
"time"
)
type TCPServer struct {
listener net.Listener
active atomic.Int64
total atomic.Int64
}
func NewTCPServer(addr string) (*TCPServer, error) {
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
return &TCPServer{listener: ln}, nil
}
func (s *TCPServer) Serve() {
for {
conn, err := s.listener.Accept()
if err != nil {
if ne, ok := err.(net.Error); ok && !ne.Temporary() {
return // listener closed
}
continue
}
s.total.Add(1)
s.active.Add(1)
go s.handleConn(conn)
}
}
func (s *TCPServer) handleConn(conn net.Conn) {
defer conn.Close()
defer s.active.Add(-1)
remote := conn.RemoteAddr()
local := conn.LocalAddr()
fmt.Printf("NEW conn %s → %s\n", remote, local)
// 30-second idle timeout — prevent zombie connections
conn.SetDeadline(time.Now().Add(30 * time.Second))
scanner := bufio.NewScanner(conn)
var bytesExchanged int64
for scanner.Scan() {
line := scanner.Text()
// Reset deadline on each message (sliding timeout)
conn.SetDeadline(time.Now().Add(30 * time.Second))
response := "ECHO: " + line + "\n"
n, err := fmt.Fprint(conn, response)
bytesExchanged += int64(len(line)) + int64(n)
if err != nil {
fmt.Printf("WRITE err %s: %v\n", remote, err)
return
}
}
if err := scanner.Err(); err != nil {
fmt.Printf("READ err %s: %v\n", remote, err)
}
fmt.Printf("CLOSE conn %s — %d bytes exchanged\n", remote, bytesExchanged)
}
func (s *TCPServer) Stats() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
fmt.Printf("STATS active=%d total=%d\n", s.active.Load(), s.total.Load())
}
}
func main() {
srv, err := NewTCPServer(":9090")
if err != nil {
fmt.Printf("listen error: %v\n", err)
return
}
fmt.Println("TCP echo server on :9090")
go srv.Stats()
srv.Serve()
}
Observe socket states:
# In another terminal while server is running
netstat -an | grep 9090
# tcp4 0 0 *.9090 *.* LISTEN
# tcp4 0 0 127.0.0.1.9090 127.0.0.1.50123 ESTABLISHED
# Connect multiple clients
for i in $(seq 1 10); do nc 127.0.0.1 9090 & done
# After killing nc clients, observe TIME_WAIT states
netstat -an | grep 9090 | grep TIME_WAIT
Real-World Context
TIME_WAIT states are normal and harmless. If you see thousands of CLOSE_WAIT states, that's an application bug — your code accepted connections but didn't call conn.Close(). In a security proxy handling 10k connections/second, even a small leak is fatal.