Skip to main content
The router is a Go-based network simulator that acts as an intermediary between clients and servers, forwarding UDP packets while simulating realistic network conditions like packet loss and variable delays.

Overview

The router enables testing of the Selective Repeat protocol under adverse network conditions by introducing controlled packet loss and delays.

Packet Forwarding

Routes packets between client and server using peer address information

Loss Simulation

Randomly drops packets based on configurable drop rate

Delay Simulation

Introduces variable delays to simulate network latency and jitter

Architecture

Client (Port 5000)                Router (Port 3000)               Server (Port 8000)
      │                                 │                                 │
      │  Packet (ToAddr=Server:8000)    │                                 │
      ├────────────────────────────────>│                                 │
      │                                 │  Transform: ToAddr → FromAddr   │
      │                                 │  Packet (FromAddr=Client:5000)  │
      │                                 ├────────────────────────────────>│
      │                                 │                                 │
      │                                 │  Packet (ToAddr=Client:5000)    │
      │                                 │<────────────────────────────────┤
      │  Packet (FromAddr=Server:8000)  │                                 │
      │<────────────────────────────────┤                                 │
The router performs address translation: the ToAddr field in incoming packets becomes the FromAddr in outgoing packets, allowing bidirectional communication.

Packet Structure

The router parses and forwards packets with the following structure:
router.go
type Packet struct {
    Type     uint8        // Packet type (1 byte)
    SeqNum   uint32       // Sequence number (4 bytes, BigEndian)
    ToAddr   *net.UDPAddr // Destination: 4 bytes IP + 2 bytes port (BigEndian)
    FromAddr *net.UDPAddr // Source (inferred, not in wire format)
    Payload  []byte       // Packet payload (variable length)
}

Wire Format

+--------+----------------+----------------+-----------+-----------+
| Type   | SeqNum         | ToAddr IP      | ToAddr    | Payload   |
|        |                |                | Port      |           |
| 1 byte | 4 bytes        | 4 bytes        | 2 bytes   | variable  |
+--------+----------------+----------------+-----------+-----------+
                          
                          All multi-byte values in BigEndian
func (p Packet) Raw() []byte {
    var buf bytes.Buffer
    append := func(data interface{}) {
        binary.Write(&buf, binary.BigEndian, data)
    }
    
    append(p.Type)
    append(p.SeqNum)
    
    // Swap ToAddr -> FromAddr and use IPv4
    append(p.FromAddr.IP.To4())
    append(uint16(p.FromAddr.Port))
    
    append(p.Payload)
    return buf.Bytes()
}

Core Functions

Packet Processing Pipeline

1

Receive

The main loop receives UDP packets from any source
router.go
for {
    buf := make([]byte, 2048)
    n, fromAddr, err := conn.ReadFromUDP(buf)
    if err != nil {
        logger.Println("failed to receive message:", err)
        continue
    }
    
    p, err := parsePacket(fromAddr, buf[:n])
    if err != nil {
        logger.Println("invalid packet:", err)
        continue
    }
    
    process(conn, *p)
}
2

Process

Apply loss and delay simulation
router.go
func process(conn *net.UDPConn, p Packet) {
    // Random packet loss
    if rand.Float64() < *dropRate {
        logger.Printf("[queue=%d] packet %s is dropped\n", currQueue(), p)
        return
    }
    
    incrQueue()
    
    // Zero or negative maxDelay: send immediately (maintain order)
    if *maxDelay <= 0 {
        send(conn, p)
        return
    }
    
    // Random delay: 0 to maxDelay
    delay := time.Duration(rand.Intn(100)) * *maxDelay / time.Duration(100)
    logger.Printf("[queue=%d] packet %s is delayed for %s\n", 
                  currQueue(), p, delay)
    
    time.AfterFunc(delay, func() {
        send(conn, p)
    })
}
3

Send

Forward packet to destination
router.go
func send(conn *net.UDPConn, p Packet) {
    decrQueue()
    if _, err := conn.WriteToUDP(p.Raw(), p.ToAddr); err != nil {
        logger.Printf("failed to deliver %s: %v\n", p, err)
        return
    }
    logger.Printf("[queue=%d] packet %s is delivered\n", currQueue(), p)
}

Configuration Options

--port
int
default:"3000"
UDP port number for the router to listen on
router --port=3000
--drop-rate
float
default:"0.0"
Probability (0.0 to 1.0) that any packet will be dropped. Use 0.0 to disable packet loss.
router --drop-rate=0.2  # Drop 20% of packets
High drop rates (>0.3) can severely impact performance and may cause connection failures if retransmission limits are reached.
--max-delay
duration
default:"0"
Maximum delay duration for any packet. Each packet is delayed by a random duration between 0 and this value. Use 0 or negative value to route packets immediately without delay (preserves packet ordering).
router --max-delay=10ms   # Delay up to 10 milliseconds
router --max-delay=1s     # Delay up to 1 second
When max-delay is 0, packets are delivered immediately without scheduling, which guarantees in-order delivery.
--seed
int64
default:"current timestamp"
Seed for the random number generator. Using the same seed produces repeatable behavior for testing.
router --seed=12345  # Reproducible random behavior

Queue Management

The router tracks the number of packets in flight using atomic operations:
router.go
var (
    queueSize = int32(0)
    incrQueue = func() int32 { return atomic.AddInt32(&queueSize, 1) }
    decrQueue = func() int32 { return atomic.AddInt32(&queueSize, -1) }
    currQueue = func() int32 { return atomic.LoadInt32(&queueSize) }
)
This queue size is logged with each packet operation:
[queue=0] packet #1 192.168.1.10:5000 -> 192.168.1.20:8000 is delayed for 5ms
[queue=1] packet #2 192.168.1.10:5000 -> 192.168.1.20:8000 is delayed for 3ms
[queue=1] packet #2 192.168.1.10:5000 -> 192.168.1.20:8000 is delivered
[queue=0] packet #1 192.168.1.10:5000 -> 192.168.1.20:8000 is delivered
The queue counter helps identify potential bottlenecks or excessive queuing during high-throughput scenarios.

Logging

The router logs all operations to both console and router.log file:
router.go
func init() {
    logf, err := os.OpenFile("router.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660)
    if err != nil {
        fmt.Fprintf(os.Stderr, "failed to open log file: %v", err)
        panic(err)
    }
    logger = log.New(
        io.MultiWriter(logf, os.Stderr), 
        "", 
        log.Ltime|log.Lmicroseconds
    )
}

Log Format

15:04:05.000000 config: drop-rate=0.10, max-delay=10ms, seed=1234567890
15:04:05.000001 router is listening at :3000
15:04:10.123456 [queue=0] packet #1 127.0.0.1:54321 -> 127.0.0.1:8000 is delayed for 7ms
15:04:10.130456 [queue=0] packet #1 127.0.0.1:54321 -> 127.0.0.1:8000 is delivered
15:04:10.234567 [queue=0] packet #2 127.0.0.1:8000 -> 127.0.0.1:54321 is dropped

Usage Examples

Perfect network conditions (testing protocol correctness):
router --port=3000 --drop-rate=0 --max-delay=0
Output:
config: drop-rate=0.00, max-delay=0s, seed=1709478234
router is listening at :3000
[queue=0] packet #1 is delivered
[queue=0] packet #2 is delivered

Address Translation

The router performs bidirectional address translation:

Client → Server

Incoming (from client):                   Outgoing (to server):
┌─────────────────────┐                  ┌─────────────────────┐
│ Type: 0 (DATA)      │                  │ Type: 0 (DATA)      │
│ SeqNum: 5           │                  │ SeqNum: 5           │
│ ToAddr: 10.0.0.2:8000│  ────────────>  │ FromAddr: 10.0.0.1:5000│
│ Payload: "hello"    │                  │ Payload: "hello"    │
└─────────────────────┘                  └─────────────────────┘
  Received from                            Sent to
  10.0.0.1:5000                           10.0.0.2:8000

Server → Client

Incoming (from server):                   Outgoing (to client):
┌─────────────────────┐                  ┌─────────────────────┐
│ Type: 1 (ACK)       │                  │ Type: 1 (ACK)       │
│ SeqNum: 5           │                  │ SeqNum: 5           │
│ ToAddr: 10.0.0.1:5000│  ────────────>  │ FromAddr: 10.0.0.2:8000│
│ Payload: empty      │                  │ Payload: empty      │
└─────────────────────┘                  └─────────────────────┘
  Received from                            Sent to
  10.0.0.2:8000                           10.0.0.1:5000
The ToAddr field in the incoming packet becomes the destination for forwarding, while the FromAddr in the outgoing packet is set to the source of the incoming packet. This allows both endpoints to communicate without knowing each other’s addresses directly.

Packet Size Constraints

router.go
const (
    minLen = 11       // Minimum: 1 + 4 + 4 + 2 = 11 bytes
    maxLen = 1024     // Maximum: 11 + 1013 bytes payload
)

Minimum Size: 11 bytes

  • Type: 1 byte
  • Sequence: 4 bytes
  • Address: 4 bytes
  • Port: 2 bytes
  • Payload: 0 bytes (e.g., ACK packets)

Maximum Size: 1024 bytes

  • Header: 11 bytes
  • Payload: up to 1013 bytes
Packets exceeding this size are rejected.

Deployment

The router is compiled for multiple platforms:
# 64-bit
./Router/linux/router_x64 --port=3000 --drop-rate=0.1 --max-delay=10ms

# 32-bit
./Router/linux/router_x86 --port=3000 --drop-rate=0.1 --max-delay=10ms

Testing Scenarios

Goal: Verify Selective Repeat implementation works correctly
router --port=3000 --drop-rate=0 --max-delay=0
Expected: All packets delivered in order, no retransmissions
Goal: Test retransmission and NACK handling
router --port=3000 --drop-rate=0.2 --max-delay=5ms --seed=100
Expected: Lost packets detected and retransmitted, all data eventually delivered
Goal: Verify selective repeat buffers out-of-order packets correctly
router --port=3000 --drop-rate=0 --max-delay=50ms
Expected: Variable delays cause packets to arrive out of order, receiver buffers and reorders them
Goal: Test protocol under high delay conditions
router --port=3000 --drop-rate=0.05 --max-delay=200ms
Expected: Timeouts may trigger unnecessary retransmissions, but data integrity maintained
Goal: Push protocol to limits
router --port=3000 --drop-rate=0.4 --max-delay=100ms
Expected: Heavy retransmissions, possible connection failures if retry limit reached

Performance Characteristics

Throughput Impact

  • No delay: Maximum throughput (limited by sliding window)
  • With delay: Throughput reduced by average delay × packet rate
  • Packet loss: Throughput = ideal × (1 - dropRate)

Latency Impact

  • Zero delay: ~1-2ms forwarding overhead
  • Max delay: Average added latency = maxDelay / 2
  • Retransmissions: Additional round-trip time per lost packet

Command Reference

router --port int --drop-rate float --max-delay duration --seed int

Options:
  --port         Port number to listen on (default: 3000)
  --drop-rate    Packet drop probability 0.0-1.0 (default: 0.0)
  --max-delay    Maximum packet delay, e.g., 5ms, 1s (default: 0)
  --seed         Random number generator seed (default: current time)

Examples:
  router --port=3000
  router --port=3000 --drop-rate=0.2 --max-delay=10ms
  router --port=3000 --drop-rate=0.1 --max-delay=20ms --seed=12345

Build docs developers (and LLMs) love