Frontend Labs
⋯

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 / ss to 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.


Generated from week5.md · 8 Oct 2026 16:36