Skip to content

KushalNaral/traceroute

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Traceroute in Go

A traceroute implementation built from scratch in Go as a learning project.

Usage

# Build
go build ./cmd/traceroute

# Run (requires root for raw ICMP sockets)
sudo ./traceroute [flags] <target>

# Examples
sudo ./traceroute google.com
sudo ./traceroute -m 15 -q 3 8.8.8.8
sudo ./traceroute -n cloudflare.com    # Disable DNS lookup

Flags

Flag Default Description
-m 30 Maximum number of hops
-w 2 Timeout per probe (seconds)
-q 3 Number of probes per hop
-n false Disable reverse DNS lookup

Sample Output

1    192.168.1.1 (router.local)       4ms    3ms    5ms
2    10.0.0.1 (isp-gateway.net)       8ms    7ms    9ms
3    *                                *      *      *
4    172.217.14.206 (lhr25s10.net)    12ms   11ms   13ms

What I Learned

1. How Traceroute Actually Works

The core trick is the TTL (Time To Live) field in IP packets:

  • Send packet with TTL=1 -> First router decrements to 0, drops it, sends back ICMP Time Exceeded
  • Send packet with TTL=2 -> First router forwards (TTL=1), second router drops it, sends Time Exceeded
  • Keep incrementing until you get ICMP Echo Reply (destination reached)

Each router that drops the packet reveals its IP address in the ICMP response.

2. Go Concurrency Patterns

Goroutines and Channels:

  • Listener runs in a separate goroutine, continuously reading packets
  • Main loop sends probes and waits on channels for responses
  • Channels act as "mailboxes" - each probe registers a channel, listener delivers to it

The Registry Pattern:

Main goroutine:                    Listener goroutine:
    |                                   |
    |-- Register(seq) -> channel        |
    |-- Send probe                      |
    |                                   |-- Receives reply
    |                                   |-- Looks up channel by seq
    |                                   |-- Delivers result
    |-- Receives from channel <---------|
    |-- Unregister(seq)                 |

sync.WaitGroup for tracking goroutines:

  • wg.Add(1) before starting goroutine
  • wg.Done() when goroutine finishes
  • wg.Wait() to block until all goroutines complete

3. Context for Cancellation

Context provides a clean way to signal "stop working":

ctx, cancel := context.WithCancel(context.Background())

// Pass ctx to all goroutines
go StartListener(ctx, ...)

// When done, call cancel()
cancel()  // All goroutines checking ctx.Done() will exit

The pattern in listener:

  • Set short read deadline (100ms)
  • After timeout, check ctx.Done()
  • If cancelled, return cleanly
  • If not, continue reading

4. Interfaces for Testability

The Problem: net.LookupAddr makes real DNS calls. Can't test easily.

The Solution: Dependency Injection via interfaces.

type Resolver interface {
    LookupAddr(string) ([]string, error)
}

// Production: uses real DNS
type NetResolver struct{}
func (n NetResolver) LookupAddr(addr string) ([]string, error) {
    return net.LookupAddr(addr)
}

// Tests: returns whatever you configure
type MockResolver struct {
    Names []string
    Err   error
}
func (m MockResolver) LookupAddr(addr string) ([]string, error) {
    return m.Names, m.Err
}

The Rule: Accept interfaces, return structs.

5. Go Project Structure

traceroute/
├── cmd/
│   └── traceroute/
│       └── main.go       # CLI entry point, flag parsing
├── internal/
│   └── tracer/
│       ├── tracer.go     # Main tracing logic
│       ├── listener.go   # ICMP packet receiver
│       ├── registry.go   # Probe correlation
│       ├── resolver.go   # DNS lookup interface
│       └── config.go     # Configuration struct
└── go.mod
  • cmd/<name>/main.go - entry points for binaries
  • internal/ - private packages, can't be imported by others
  • Keep main.go thin - just wiring, no business logic

6. Table-Driven Tests

Instead of writing repetitive tests:

func TestEncodeSequence(t *testing.T) {
    tests := []struct {
        name         string
        ttl          int
        probe        int
        probesPerHop int
        expected     int
    }{
        {"ttl=1, probe=0", 1, 0, 3, 0},
        {"ttl=1, probe=1", 1, 1, 3, 1},
        {"ttl=2, probe=0", 2, 0, 3, 3},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := EncodeSequence(tt.ttl, tt.probesPerHop, tt.probe)
            if result != tt.expected {
                t.Errorf("expected %d, got %d", tt.expected, result)
            }
        })
    }
}

7. Signal Handling

Graceful shutdown on Ctrl+C:

func handleSignals(cancel context.CancelFunc) {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    <-sigs  // Block until signal
    cancel()  // Trigger cancellation
}

Implementation Journey

Phase 1: Basic ICMP

  1. Opened ICMP socket with icmp.ListenPacket
  2. Sent Echo Request to localhost
  3. Received and parsed Echo Reply
  4. Key learning: Understanding golang.org/x/net/icmp and ipv4 packages

Phase 2: TTL and Time Exceeded

  1. Used ipv4.PacketConn.SetTTL() to control hop count
  2. Sent probes with incrementing TTL
  3. Parsed Time Exceeded messages from intermediate routers
  4. Key learning: Extracting sequence number from embedded ICMP header in Time Exceeded

Phase 3: Concurrent Listener

  1. Moved packet reading to separate goroutine
  2. Created ProbeRegistry with map[seq]channel
  3. Implemented Register/Deliver/Unregister pattern
  4. Key learning: Go's channel-based coordination between goroutines

Phase 4: RTT Measurement

  1. Store time.Now() when registering probe
  2. Compute time.Since(sentAt) when delivering result
  3. Key learning: Measuring time across goroutines

Phase 5: CLI Arguments

  1. Used flag package for -m, -w, -q flags
  2. Positional argument from flag.Args()
  3. net.ResolveIPAddr() for hostname resolution
  4. Key learning: Go's flag parsing, positional args come after flag.Parse()

Phase 6: Multiple Probes Per Hop

  1. Inner loop for N probes per TTL
  2. Unique sequence numbers: seq = (ttl-1)*probesPerHop + probe
  3. Collect results into slice before printing
  4. Key learning: Separating data collection from output

Phase 7: Graceful Shutdown

  1. Added context.Context throughout
  2. Listener checks ctx.Done() after read timeouts
  3. Signal handler calls cancel() on Ctrl+C
  4. Key learning: Context propagation for cancellation

Phase 8: Reverse DNS

  1. Created Resolver interface for testability
  2. net.LookupAddr() for PTR records
  3. Format: IP (hostname) or just IP if no PTR
  4. Key learning: Dependency injection via interfaces

Phase 9: Tests

  1. Table-driven tests for sequence encoding
  2. Mock resolver for DNS tests
  3. WaitGroup + timeout pattern for listener cancellation test
  4. Key learning: Testing concurrent code, mocking external dependencies

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        main.go                              │
│  - Parse flags                                              │
│  - Create context                                           │
│  - Setup signal handler                                     │
│  - Call ListenTrace()                                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                     ListenTrace()                           │
│                                                             │
│  ┌─────────────┐     ┌─────────────────────────────────┐   │
│  │  Listener   │     │         Main Loop               │   │
│  │  goroutine  │     │                                 │   │
│  │             │     │  for ttl := 1 to maxHops:       │   │
│  │  - ReadFrom │     │    for probe := 0 to N:         │   │
│  │  - Parse    │     │      Register(seq)              │   │
│  │  - Deliver  │────▶│      Send probe                 │   │
│  │             │     │      Wait on channel            │   │
│  └─────────────┘     │      Collect result             │   │
│         ▲            │    Print hop results            │   │
│         │            │    Check if destination         │   │
│         │            └─────────────────────────────────┘   │
│         │                                                   │
│  ┌──────┴──────────────────────────────────────────────┐   │
│  │                  ProbeRegistry                       │   │
│  │   map[seq] -> { channel, sentAt }                   │   │
│  │   Register() / Deliver() / Unregister()             │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Running Tests

# Run all tests
go test ./internal/tracer -v

# Run specific test
go test ./internal/tracer -v -run "TestDNSResolver"

# Run listener test (requires root)
sudo go test ./internal/tracer -v -run "TestListenerCancellation"

Key Files

File Purpose
cmd/traceroute/main.go CLI entry point, flag parsing, signal handling
internal/tracer/tracer.go Main tracing loop, probe sending
internal/tracer/listener.go ICMP packet receiver, message parsing
internal/tracer/registry.go Probe correlation, RTT tracking
internal/tracer/resolver.go DNS lookup interface
internal/tracer/config.go Configuration struct

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors