Using socat, Linux traffic control (tc) and netem, we can emulate realistic satellite link network conditions. The great thing about this approach is that it’s completely portable - you can drop it in front of any service without modifying the target application.

Overview

  • Realistic Network Emulation: Simulate various satellite scenarios from LEO to GEO
  • Real-time Monitoring: Expose metrics via Prometheus and visualize in Grafana
  • Runtime Control: Change network conditions on the fly
  • Portable Proxy Design: Drop-in containerized solution that works with any service

Technical Implementation

Here’s how to build a network emulator using common Linux tools and containers:

Containerized Proxy Architecture

This containerized approach provides several benefits:

  1. Portable: Run it locally, in CI/CD, or in production
  2. Isolated: Network conditions don’t affect the host system
  3. Self-contained: Includes all necessary tools and monitoring

Implement as a self-contained Docker container that acts as a transparent proxy:

services:
  # Your target application
  app:
    image: your-app:latest

  # Network emulator proxy
  emulator:
    image: network-emulator:latest
    environment:
      - UPSTREAM_HOST=app
      - MODE=cycle
      - CYCLE_SCENARIOS=low_latency,high_latency
    ports:
      - "80:80"
      - "443:443"
    cap_add:
      - NET_ADMIN
    depends_on:
      - app

  # Optional monitoring
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./monitoring/prometheus:/etc/prometheus
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    volumes:
      - ./monitoring/grafana:/var/lib/grafana
    ports:
      - "3000:3000"

Note: The emulator container needs NET_ADMIN to modify network interfaces.

Network Proxy with socat

At the heart of the emulator is socat, a flexible, multi-purpose relay tool. It acts as a transparent proxy, forwarding traffic between the client and a specified upstream host:

socat -v TCP-LISTEN:80,fork,reuseaddr TCP:${UPSTREAM_HOST}:80

Network Emulation with tc netem

The network conditions are applied using Linux’s traffic control (tc) with the netem module. Here’s how different satellite scenarios are implemented:

# LEO satellite with good conditions
tc qdisc add dev eth0 root netem \
  delay 400ms 30ms \
  loss 0.5% \
  rate 9mbit

# GEO satellite during heavy rain
tc qdisc add dev eth0 root netem \
  delay 600ms 100ms \
  loss 10% \
  corrupt 2% \
  rate 2mbit

We can dynamically update the conditions using a control script to monitor a named pipe for commands during runtime:

# Switch to heavy rain scenario
echo "set heavy_rain_satellite" > /tmp/netem_control

# Start automatic cycling
echo "cycle" > /tmp/netem_control

# Remove all network conditions
echo "set none" > /tmp/netem_control
# Create control pipe
mkfifo /tmp/netem_control

# Monitor for commands
while true; do
  if read command < /tmp/netem_control; then
    case "$command" in
      "set "*)
        scenario="${command#set }"
        apply_scenario "$scenario"
        ;;
      "cycle")
        start_cycle
        ;;
    esac
  fi
done

Metrics Collection

Handle metrics collection via a custom exporter that provides Prometheus-compatible metrics, see trivial Golang example below:

package main

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "net/http"
    "os/exec"
    "regexp"
    "strconv"
    "time"
)

var (
    networkDelay = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "network_delay_ms",
        Help: "Current network delay in milliseconds",
    })

    packetLoss = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "packet_loss_percent",
        Help: "Current packet loss percentage",
    })

    bandwidth = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "bandwidth_kbps",
        Help: "Current bandwidth in Kbps",
    })
)

func init() {
    prometheus.MustRegister(networkDelay)
    prometheus.MustRegister(packetLoss)
    prometheus.MustRegister(bandwidth)
}

func collectMetrics() {
    for {
        cmd := exec.Command("tc", "-s", "qdisc", "show", "dev", "eth0")
        output, err := cmd.Output()
        if err != nil {
            continue
        }

        // Parse tc output using regexp
        if delay := parseDelay(string(output)); delay != nil {
            networkDelay.Set(*delay)
        }
        if loss := parseLoss(string(output)); loss != nil {
            packetLoss.Set(*loss)
        }
        if bw := parseBandwidth(string(output)); bw != nil {
            bandwidth.Set(*bw)
        }

        time.Sleep(1 * time.Second)
    }
}

func main() {
    go collectMetrics()

    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":9091", nil)
}

Metrics are exposed via an endpoint (/metrics) in the standard Prometheus format:

# HELP network_delay_ms Current network delay in milliseconds
# TYPE network_delay_ms gauge
network_delay_ms 600.0
# HELP packet_loss_percent Current packet loss percentage
# TYPE packet_loss_percent gauge
packet_loss_percent 1.0
# HELP bandwidth_kbps Current bandwidth in Kbps
# TYPE bandwidth_kbps gauge
bandwidth_kbps 2048.0

These metrics are then scraped by Prometheus and visualized in Grafana, providing real-time insights into the network conditions:

scrape_configs:
  - job_name: "satellite-emulator"
    static_configs:
      - targets: ["localhost:9091"]
    scrape_interval: 1s

Use Cases

This pattern can be useful for:

  • Testing application behavior under various network conditions
  • Evaluating protocol performance
  • Automated testing in CI/CD pipelines