commit a221870b70459b05dba6c2ff10d5f2ff6ef8fa2b Author: Dongho Kim Date: Sun May 17 16:33:43 2026 +0200 first diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5829d1 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# Drone MP-QUIC Measurement Testbed + +This testbed is designed to measure highly reliable (99.999%), ultra-low latency MP-QUIC communication between a Drone and a Ground Station over heterogeneous links (5G, Satellite, Mesh). + +## 1. Deployment Guide + +The testbed is split into two independent deployable units. + +### Prerequisites +- **Go 1.20+**: To build the client and server applications. +- **Python 3.x**: With `pandas`, `matplotlib`, and `seaborn` installed for analysis. +- **Linux (Optional)**: If you wish to run the eBPF kernel tracing, you must be on a Linux machine with `bcc` installed and run the scripts as `root`. + +### Running the Ground Station (Server) +Deploy the `server/` directory to the Ground Station machine. +```bash +# This automatically builds the Go code and starts listening on port 4242. +# It logs the application-level delivery metrics (packet loss, latency) to ground_metrics.csv +./server/scripts/run.sh --addr 0.0.0.0:4242 --output ground_metrics.csv +``` + +### Running the Drone (Client) +Deploy the `client/` directory to the Drone machine. +```bash +# Blasts framed payloads to the ground station. +# You can specify the scheduler, the duration in seconds, and the payload size in bytes. +./client/scripts/run.sh \ + --addr :4242 \ + --scheduler minrtt \ + --duration 30 \ + --payload-size 2048 +``` + +### Analyzing the Results +Once the test finishes, copy `ground_metrics.csv` to your laptop and run: +```bash +pip install pandas matplotlib seaborn +python3 analysis/visualize.py --app ground_metrics.csv +``` +This will print your exact **Reliability %** to the terminal and generate scatter plots of the jitter and glass-to-glass latency. + +--- + +## 2. Developer Guide: Adding Custom Algorithms + +This testbed is designed to be highly modular so you can test custom Path Schedulers and Congestion Control algorithms. + +### How to Add a Custom Path Scheduler + +Path Schedulers dictate *which* link (5G, Mesh, Satellite) a packet should be sent over. The testbed uses a dynamic registry, meaning you never have to modify `client/main.go` to add a new scheduler. + +1. Navigate to the `client/scheduler/` directory. +2. Copy the template file: `cp custom_example.go my_scheduler.go`. +3. Open `my_scheduler.go` and implement the `quic.PathScheduler` interface, which requires three methods: + - `SelectPath(...)`: Contains your custom logic to choose a path. + - `UpdateQuota(...)`: Tracks how much data was sent. + - `Reset()`: Resets your internal metrics. +4. At the bottom of `my_scheduler.go`, update the `init()` function to register your new scheduler: + ```go + func init() { + Register("my_custom_algo", func() quic.PathScheduler { + return &MyScheduler{} + }) + } + ``` +5. Run `./client/scripts/run.sh --list-schedulers` and you will see `my_custom_algo` automatically available! + +### How to Add Custom Congestion Control + +Unlike Schedulers, **Congestion Control (CC) algorithms are deeply embedded inside the `mp-quic-go` library**. The library currently supports `CUBIC` and `OLIA`. + +To write a completely new CC algorithm, you must modify the library itself. Here is the easiest workflow: + +**Step 1: Fork the Library Locally** +Clone the underlying library to your machine: +```bash +cd ~ +git clone https://github.com/AeonDave/mp-quic-go.git +``` + +**Step 2: Tell Go to use your Local Copy** +In this `drone` project, tell Go to use your local copy instead of downloading it from Github: +```bash +cd drone +go mod edit -replace github.com/AeonDave/mp-quic-go=/path/to/your/local/mp-quic-go +go mod tidy +``` + +**Step 3: Write your CC Algorithm** +Open your local `mp-quic-go` code and implement the `congestion.SendAlgorithm` interface. +You can look at `internal/congestion/cubic_sender.go` inside the library as a template. + +**Step 4: Wire it into the Multipath Controller** +Inside the library, open `multipath_controller.go`. You will need to modify the `MultipathController` to instantiate your new CC algorithm for new paths, similar to how it currently enables OLIA when `EnableOLIA()` is called. + +Once your custom library modifications are done, you simply run `./client/scripts/run.sh` inside the `drone` directory, and Go will automatically compile your local, modified library into the drone client! diff --git a/__pycache__/run_experiment.cpython-314.pyc b/__pycache__/run_experiment.cpython-314.pyc new file mode 100644 index 0000000..5ea5bbf Binary files /dev/null and b/__pycache__/run_experiment.cpython-314.pyc differ diff --git a/analysis/visualize.py b/analysis/visualize.py new file mode 100755 index 0000000..f4e0a0d --- /dev/null +++ b/analysis/visualize.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""MP-QUIC Application & eBPF Results Visualizer. + +This script takes the CSV outputs and plots their latency distributions and timelines. + +Usage: + python visualize.py --app ../server/app_metrics.csv + python visualize.py --client ../results/client.csv --server ../results/server.csv +""" + +import argparse +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import os + +def load_ebpf_data(csv_path, label): + if not os.path.exists(csv_path): + print(f"Warning: File {csv_path} not found.") + return pd.DataFrame() + + df = pd.read_csv(csv_path) + df['node'] = label + df = df.sort_values('timestamp') + if not df.empty: + first_event_time = df['timestamp'].iloc[0] + df['rel_time'] = df['timestamp'] - first_event_time + else: + df['rel_time'] = 0.0 + df['value_us'] = df['value_ns'] / 1000.0 + return df + +def plot_app_metrics(csv_path, output_dir): + """Plot Application-Level Metrics (Drone -> Ground Station).""" + if not os.path.exists(csv_path): + print(f"App metrics file {csv_path} not found.") + return + + df = pd.read_csv(csv_path) + if df.empty: + print("App metrics file is empty.") + return + + df = df.sort_values('sequence_number') + + # Calculate Jitter and Rel Latency + df['latency_ms'] = df['latency_ns'] / 1000000.0 + + # Relative time from first packet received + df['rel_time'] = (df['ground_recv_time'] - df['ground_recv_time'].min()) / 1e9 + + plt.figure(figsize=(10, 5)) + sns.scatterplot(x='rel_time', y='latency_ms', data=df, s=15, alpha=0.6) + plt.title('App-Level End-to-End Latency Over Time (Glass-to-Glass)') + plt.ylabel('Relative Latency (ms)') + plt.xlabel('Time (s)') + out_path = os.path.join(output_dir, 'app_latency_timeline.png') + plt.savefig(out_path, dpi=300, bbox_inches='tight') + plt.close() + + # Packet Loss Calculation + max_seq = df['sequence_number'].max() + min_seq = df['sequence_number'].min() + expected_packets = max_seq - min_seq + 1 + received_packets = len(df) + lost_packets = expected_packets - received_packets + reliability = (received_packets / expected_packets) * 100 if expected_packets > 0 else 0 + + print("\n" + "=" * 55) + print(" ๐Ÿ† APP-LEVEL RELIABILITY (Drone -> Ground)") + print("=" * 55) + print(f" Packets Sent (Expected): {expected_packets}") + print(f" Packets Received: {received_packets}") + print(f" Packets Lost: {lost_packets}") + print(f" Reliability: {reliability:.5f}%") + print("=" * 55) + + print(f"Saved app-level plot to {out_path}") + + +def plot_latency_distributions(df, output_dir): + latency_df = df[df['event_type'].isin(['SEND_LATENCY', 'RECV_LATENCY'])] + if latency_df.empty: return + + plt.figure(figsize=(10, 6)) + sns.violinplot(x='event_type', y='value_us', hue='node', data=latency_df, split=True, inner="quartile") + plt.yscale('log') + plt.title('Kernel Network Stack Latency Distribution (Log Scale)') + plt.ylabel('Latency (ยตs)') + plt.xlabel('Event Type') + + out_path = os.path.join(output_dir, 'kernel_latency_distribution.png') + plt.savefig(out_path, dpi=300, bbox_inches='tight') + plt.close() + print(f"Saved kernel distribution plot to {out_path}") + +def plot_latency_timeline(df, output_dir): + latency_df = df[df['event_type'].isin(['SEND_LATENCY', 'RECV_LATENCY'])] + if latency_df.empty: return + + g = sns.FacetGrid(latency_df, col="event_type", row="node", margin_titles=True, height=4, aspect=2) + g.map(sns.scatterplot, "rel_time", "value_us", alpha=0.5, s=10) + g.set_axis_labels("Time (s)", "Latency (ยตs)") + g.set_titles(col_template="{col_name}", row_template="{row_name}") + + for ax in g.axes.flat: + ax.set_yscale('log') + + g.fig.suptitle('Kernel Latency Over Time', y=1.02) + + out_path = os.path.join(output_dir, 'kernel_latency_timeline.png') + plt.savefig(out_path, dpi=300, bbox_inches='tight') + plt.close() + print(f"Saved kernel timeline plot to {out_path}") + +def main(): + parser = argparse.ArgumentParser(description="Visualize MP-QUIC data") + parser.add_argument("--client", help="Path to client eBPF CSV file") + parser.add_argument("--server", help="Path to server eBPF CSV file") + parser.add_argument("--app", help="Path to App-Level metrics CSV file (e.g. app_metrics.csv)") + parser.add_argument("--outdir", default="plots", help="Directory to save plots") + args = parser.parse_args() + + os.makedirs(args.outdir, exist_ok=True) + + if args.app: + plot_app_metrics(args.app, args.outdir) + + dfs = [] + if args.client: + dfs.append(load_ebpf_data(args.client, "Client")) + if args.server: + dfs.append(load_ebpf_data(args.server, "Server")) + + if dfs: + df = pd.concat(dfs, ignore_index=True) + sns.set_theme(style="whitegrid") + plot_latency_distributions(df, args.outdir) + plot_latency_timeline(df, args.outdir) + + print("\nVisualization complete! Check the '{}' directory.".format(args.outdir)) + +if __name__ == "__main__": + main() diff --git a/bin/client b/bin/client new file mode 100755 index 0000000..648eac3 Binary files /dev/null and b/bin/client differ diff --git a/bin/server b/bin/server new file mode 100755 index 0000000..37e02c9 Binary files /dev/null and b/bin/server differ diff --git a/client/ebpf/measure.py b/client/ebpf/measure.py new file mode 100644 index 0000000..5558a17 --- /dev/null +++ b/client/ebpf/measure.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""eBPF-based UDP tracer for MP-QUIC performance measurement. + +Traces kernel-level UDP send/receive latency and packet drops. +Requires root privileges and Linux with BCC installed. +""" + +from bcc import BPF +import time +import argparse +import signal +import sys + +bpf_text = """ +#include +#include +#include +#include + +#define MPQUIC_PORT __TARGET_PORT__ + +BPF_HASH(send_start, u32, u64); +BPF_HASH(recv_start, u32, u64); + +BPF_PERF_OUTPUT(events); + +struct event_t { + u64 timestamp_ns; + u64 delta_ns; + u32 pid; + u16 sport; + u16 dport; + u8 event_type; // 0=SEND, 1=RECV, 2=DROP +}; + +int trace_udp_sendmsg(struct pt_regs *ctx, struct sock *sk) { + struct inet_sock *inet = (struct inet_sock *)sk; + u16 dport = 0; + bpf_probe_read_kernel(&dport, sizeof(dport), &inet->inet_dport); + dport = ntohs(dport); + + // Filter: only trace traffic to/from MP-QUIC port + u16 sport = 0; + bpf_probe_read_kernel(&sport, sizeof(sport), &inet->inet_sport); + sport = ntohs(sport); + + if (dport != MPQUIC_PORT && sport != MPQUIC_PORT) + return 0; + + u32 pid = bpf_get_current_pid_tgid(); + u64 ts = bpf_ktime_get_ns(); + send_start.update(&pid, &ts); + return 0; +} + +int trace_udp_sendmsg_ret(struct pt_regs *ctx) { + u32 pid = bpf_get_current_pid_tgid(); + u64 *tsp = send_start.lookup(&pid); + if (tsp == 0) + return 0; + + u64 delta = bpf_ktime_get_ns() - *tsp; + send_start.delete(&pid); + + struct event_t event = {}; + event.timestamp_ns = bpf_ktime_get_ns(); + event.delta_ns = delta; + event.pid = pid; + event.event_type = 0; // SEND + events.perf_submit(ctx, &event, sizeof(event)); + return 0; +} + +int trace_udp_recvmsg(struct pt_regs *ctx, struct sock *sk) { + struct inet_sock *inet = (struct inet_sock *)sk; + u16 sport = 0; + bpf_probe_read_kernel(&sport, sizeof(sport), &inet->inet_sport); + sport = ntohs(sport); + + u16 dport = 0; + bpf_probe_read_kernel(&dport, sizeof(dport), &inet->inet_dport); + dport = ntohs(dport); + + if (sport != MPQUIC_PORT && dport != MPQUIC_PORT) + return 0; + + u32 pid = bpf_get_current_pid_tgid(); + u64 ts = bpf_ktime_get_ns(); + recv_start.update(&pid, &ts); + return 0; +} + +int trace_udp_recvmsg_ret(struct pt_regs *ctx) { + u32 pid = bpf_get_current_pid_tgid(); + u64 *tsp = recv_start.lookup(&pid); + if (tsp == 0) + return 0; + + u64 delta = bpf_ktime_get_ns() - *tsp; + recv_start.delete(&pid); + + struct event_t event = {}; + event.timestamp_ns = bpf_ktime_get_ns(); + event.delta_ns = delta; + event.pid = pid; + event.event_type = 1; // RECV + events.perf_submit(ctx, &event, sizeof(event)); + return 0; +} + +TRACEPOINT_PROBE(skb, kfree_skb) { + struct event_t event = {}; + event.timestamp_ns = bpf_ktime_get_ns(); + event.delta_ns = 0; + event.pid = bpf_get_current_pid_tgid(); + event.event_type = 2; // DROP + events.perf_submit(ctx, &event, sizeof(event)); + return 0; +} +""" + +EVENT_TYPES = {0: "SEND_LATENCY", 1: "RECV_LATENCY", 2: "DROP"} + +# Counters for live summary +stats = {"send_count": 0, "recv_count": 0, "drop_count": 0, + "send_total_ns": 0, "recv_total_ns": 0} + + +def main(): + parser = argparse.ArgumentParser(description="eBPF UDP Tracer for MP-QUIC") + parser.add_argument("--output", type=str, default="measurement.csv", + help="CSV output file") + parser.add_argument("--port", type=int, default=4242, + help="MP-QUIC port to filter on") + args = parser.parse_args() + + program = bpf_text.replace("__TARGET_PORT__", str(args.port)) + + print(f"Compiling eBPF program (requires root)...") + print(f"Filtering on UDP port {args.port}") + b = BPF(text=program) + + # Attach kprobes for send and receive paths + b.attach_kprobe(event="udp_sendmsg", fn_name="trace_udp_sendmsg") + b.attach_kretprobe(event="udp_sendmsg", fn_name="trace_udp_sendmsg_ret") + b.attach_kprobe(event="udp_recvmsg", fn_name="trace_udp_recvmsg") + b.attach_kretprobe(event="udp_recvmsg", fn_name="trace_udp_recvmsg_ret") + + csv_file = open(args.output, "w") + csv_file.write("timestamp,event_type,value_ns,pid\n") + + def handle_event(cpu, data, size): + event = b["events"].event(data) + etype = EVENT_TYPES.get(event.event_type, "UNKNOWN") + csv_file.write(f"{time.time()},{etype},{event.delta_ns},{event.pid}\n") + + # Update live stats + if event.event_type == 0: + stats["send_count"] += 1 + stats["send_total_ns"] += event.delta_ns + elif event.event_type == 1: + stats["recv_count"] += 1 + stats["recv_total_ns"] += event.delta_ns + elif event.event_type == 2: + stats["drop_count"] += 1 + + b["events"].open_perf_buffer(handle_event) + + def signal_handler(sig, frame): + print("\n--- Measurement Summary ---") + if stats["send_count"] > 0: + avg_send = stats["send_total_ns"] / stats["send_count"] / 1000 + print(f" Send events: {stats['send_count']:>8} (avg {avg_send:.1f} ยตs)") + if stats["recv_count"] > 0: + avg_recv = stats["recv_total_ns"] / stats["recv_count"] / 1000 + print(f" Recv events: {stats['recv_count']:>8} (avg {avg_recv:.1f} ยตs)") + print(f" Drop events: {stats['drop_count']:>8}") + print(f" Output file: {args.output}") + csv_file.close() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + print(f"Tracing... Output โ†’ {args.output}. Ctrl-C to stop.") + while True: + b.perf_buffer_poll() + + +if __name__ == "__main__": + main() diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..86eb1dd --- /dev/null +++ b/client/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "crypto/tls" + "encoding/binary" + "flag" + "fmt" + "log" + "strings" + "time" + + quic "github.com/AeonDave/mp-quic-go" + "mpquic-exp/client/scheduler" +) + +func main() { + schedName := flag.String("scheduler", "roundrobin", "Scheduler to use") + listScheds := flag.Bool("list-schedulers", false, "List available schedulers") + addr := flag.String("addr", "127.0.0.1:4242", "Server address") + duration := flag.Int("duration", 10, "Duration in seconds") + payloadSize := flag.Int("payload-size", 1024, "Size of the payload in bytes (min 20)") + flag.Parse() + + if *listScheds { + fmt.Println("Available schedulers:") + for _, name := range scheduler.List() { + fmt.Println(" -", name) + } + return + } + + if *payloadSize < 20 { + *payloadSize = 20 // 4 bytes length, 8 bytes seq, 8 bytes timestamp + } + + tlsConf := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"mpquic-exp"}, + } + + sched, err := scheduler.Get(*schedName) + if err != nil { + log.Fatalf("Error: %v. Available: %s", err, strings.Join(scheduler.List(), ", ")) + } + + quicConfig := &quic.Config{ + MaxPaths: 4, + MultipathController: quic.NewDefaultMultipathController(sched), + } + + fmt.Printf("Connecting to %s using %s scheduler...\n", *addr, *schedName) + conn, err := quic.DialAddr(context.Background(), *addr, tlsConf, quicConfig) + if err != nil { + log.Fatal(err) + } + + stream, err := conn.OpenStreamSync(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Stream opened, sending data for %d seconds (Payload Size: %d bytes)...\n", *duration, *payloadSize) + + end := time.Now().Add(time.Duration(*duration) * time.Second) + payload := make([]byte, *payloadSize) + + // Frame structure: + // [0:4] uint32 Total Length + // [4:12] uint64 Sequence Number + // [12:20] uint64 Send Timestamp (nanoseconds) + // [20:] Padding (dummy data) + binary.BigEndian.PutUint32(payload[0:4], uint32(*payloadSize)) + + var seqNum uint64 = 0 + totalBytes := 0 + + // Target sending rate: we don't want to lock the CPU entirely in a busy loop. + // We yield slightly to allow the network stack to process. + // But to measure max throughput we just send as fast as stream.Write allows. + for time.Now().Before(end) { + seqNum++ + sendTime := uint64(time.Now().UnixNano()) + + binary.BigEndian.PutUint64(payload[4:12], seqNum) + binary.BigEndian.PutUint64(payload[12:20], sendTime) + + n, err := stream.Write(payload) + if err != nil { + log.Fatal("Stream write error:", err) + } + totalBytes += n + } + + fmt.Printf("Finished sending %d packets, %d bytes (%.2f MB)\n", seqNum, totalBytes, float64(totalBytes)/1024/1024) +} diff --git a/client/scheduler/custom_example.go b/client/scheduler/custom_example.go new file mode 100644 index 0000000..d5b851d --- /dev/null +++ b/client/scheduler/custom_example.go @@ -0,0 +1,100 @@ +package scheduler + +// This file demonstrates how to add a custom scheduler. +// To create your own: +// 1. Copy this file and rename it (e.g., weighted_rtt.go) +// 2. Implement the three PathScheduler methods +// 3. Update the init() to register with your scheduler name +// +// The scheduler will automatically appear in --list-schedulers. + +import ( + "sync" + + quic "github.com/AeonDave/mp-quic-go" +) + +func init() { + Register("weighted", func() quic.PathScheduler { + return NewWeightedScheduler() + }) +} + +// WeightedScheduler is an example custom scheduler that weighs paths +// by a combination of RTT and available congestion window. +type WeightedScheduler struct { + mu sync.Mutex + quotas map[quic.PathID]uint64 +} + +// NewWeightedScheduler creates a new weighted scheduler. +func NewWeightedScheduler() *WeightedScheduler { + return &WeightedScheduler{ + quotas: make(map[quic.PathID]uint64), + } +} + +// SelectPath picks the path with the best weighted score. +// Score = (CongestionWindow - BytesInFlight) / (1 + SmoothedRTT_ms) +// Higher score = more capacity available per unit latency. +func (s *WeightedScheduler) SelectPath(paths []quic.SchedulerPathInfo, hasRetransmission bool) *quic.SchedulerPathInfo { + s.mu.Lock() + defer s.mu.Unlock() + + if len(paths) == 0 { + return nil + } + if len(paths) == 1 { + if !hasRetransmission && !paths[0].SendingAllowed { + return nil + } + return &paths[0] + } + + var best *quic.SchedulerPathInfo + var bestScore float64 = -1 + + for i := range paths { + p := &paths[i] + if !hasRetransmission && !p.SendingAllowed { + continue + } + if p.PotentiallyFailed { + continue + } + + // Available window + available := float64(0) + if p.CongestionWindow > p.BytesInFlight { + available = float64(p.CongestionWindow - p.BytesInFlight) + } + + // RTT factor (ms, minimum 1 to avoid division by zero) + rttMs := float64(1) + if p.SmoothedRTT.Milliseconds() > 0 { + rttMs = float64(p.SmoothedRTT.Milliseconds()) + } + + score := available / rttMs + if score > bestScore { + bestScore = score + best = p + } + } + + return best +} + +// UpdateQuota tracks per-path send counts. +func (s *WeightedScheduler) UpdateQuota(pathID quic.PathID, packetSize quic.ByteCount) { + s.mu.Lock() + defer s.mu.Unlock() + s.quotas[pathID]++ +} + +// Reset clears all state. +func (s *WeightedScheduler) Reset() { + s.mu.Lock() + defer s.mu.Unlock() + s.quotas = make(map[quic.PathID]uint64) +} diff --git a/client/scheduler/lowlatency.go b/client/scheduler/lowlatency.go new file mode 100644 index 0000000..4010b47 --- /dev/null +++ b/client/scheduler/lowlatency.go @@ -0,0 +1,9 @@ +package scheduler + +import quic "github.com/AeonDave/mp-quic-go" + +func init() { + Register("lowlatency", func() quic.PathScheduler { + return quic.NewLowLatencyScheduler() + }) +} diff --git a/client/scheduler/minrtt.go b/client/scheduler/minrtt.go new file mode 100644 index 0000000..aead772 --- /dev/null +++ b/client/scheduler/minrtt.go @@ -0,0 +1,11 @@ +package scheduler + +import quic "github.com/AeonDave/mp-quic-go" + +func init() { + Register("minrtt", func() quic.PathScheduler { + // rttBias=0.8 favors lower RTT paths while still considering load balance. + // Adjust this value: 1.0=pure RTT, 0.0=pure load balancing. + return quic.NewMinRTTScheduler(0.8) + }) +} diff --git a/client/scheduler/registry.go b/client/scheduler/registry.go new file mode 100644 index 0000000..6cebe4f --- /dev/null +++ b/client/scheduler/registry.go @@ -0,0 +1,60 @@ +// Package scheduler provides a modular registry for MP-QUIC path schedulers. +// +// Adding a new scheduler: +// 1. Create a new file in this package (e.g., myscheduler.go) +// 2. Implement quic.PathScheduler (SelectPath, UpdateQuota, Reset) +// 3. In an init() function, call Register("myscheduler", factory) +// +// See custom_example.go for a complete example. +package scheduler + +import ( + "fmt" + "sort" + "sync" + + quic "github.com/AeonDave/mp-quic-go" +) + +// Factory is a function that creates a new PathScheduler instance. +type Factory func() quic.PathScheduler + +var ( + mu sync.RWMutex + factories = make(map[string]Factory) +) + +// Register adds a scheduler factory to the registry. +// Call this from init() in each scheduler file. +func Register(name string, factory Factory) { + mu.Lock() + defer mu.Unlock() + if _, exists := factories[name]; exists { + panic(fmt.Sprintf("scheduler: duplicate registration for %q", name)) + } + factories[name] = factory +} + +// Get creates a new instance of the named scheduler. +// Returns an error if the name is not registered. +func Get(name string) (quic.PathScheduler, error) { + mu.RLock() + defer mu.RUnlock() + factory, exists := factories[name] + if !exists { + return nil, fmt.Errorf("scheduler: unknown scheduler %q (available: %v)", name, List()) + } + return factory(), nil +} + +// List returns sorted names of all registered schedulers. +func List() []string { + mu.RLock() + defer mu.RUnlock() + names := make([]string, 0, len(factories)) + for name := range factories { + names = append(names, name) + } + sort.Strings(names) + return names +} diff --git a/client/scheduler/roundrobin.go b/client/scheduler/roundrobin.go new file mode 100644 index 0000000..fbb4ab6 --- /dev/null +++ b/client/scheduler/roundrobin.go @@ -0,0 +1,9 @@ +package scheduler + +import quic "github.com/AeonDave/mp-quic-go" + +func init() { + Register("roundrobin", func() quic.PathScheduler { + return quic.NewRoundRobinScheduler() + }) +} diff --git a/client/scripts/run.sh b/client/scripts/run.sh new file mode 100755 index 0000000..75c47a6 --- /dev/null +++ b/client/scripts/run.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Build the client +echo "Building client..." +go build -o client_bin main.go + +echo "Starting client..." +./client_bin "$@" diff --git a/client/scripts/setup.sh b/client/scripts/setup.sh new file mode 100644 index 0000000..e570bad --- /dev/null +++ b/client/scripts/setup.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Setup script for MP-QUIC experiment environment. +# Run on each Linux machine (server + client) before experiments. +set -euo pipefail + +echo "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" +echo "โ•‘ MP-QUIC Experiment Environment Setup โ•‘" +echo "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + +# --- OS Check --- +if [[ ! -f /etc/os-release ]]; then + echo "โŒ Not running on Linux. eBPF tracing requires Linux." + echo " You can still build and run the Go binaries on this machine." + exit 1 +fi + +. /etc/os-release +echo "๐Ÿ“‹ Detected OS: $PRETTY_NAME" + +# --- Install dependencies --- +if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then + echo "" + echo "๐Ÿ“ฆ Installing packages..." + sudo apt-get update -qq + sudo apt-get install -y -qq \ + bpfcc-tools \ + python3-bpfcc \ + linux-headers-$(uname -r) \ + golang-go \ + iperf3 \ + net-tools \ + iproute2 + + # Ensure Python3 BCC bindings work + python3 -c "from bcc import BPF; print(' โœ“ BCC Python bindings OK')" 2>/dev/null || { + echo " โš  BCC Python import failed. Try: sudo apt install python3-bpfcc" + } +elif [[ "$ID" == "fedora" || "$ID" == "rhel" || "$ID" == "centos" ]]; then + echo "" + echo "๐Ÿ“ฆ Installing packages (DNF)..." + sudo dnf install -y \ + bcc-tools \ + python3-bcc \ + kernel-devel-$(uname -r) \ + golang \ + iperf3 \ + iproute +else + echo "โš  Unsupported distro: $ID" + echo " Please install manually: bcc-tools, python3-bcc, golang, linux-headers" +fi + +# --- Verify Go --- +echo "" +if command -v go &>/dev/null; then + echo "โœ“ Go $(go version | awk '{print $3}')" +else + echo "โŒ Go not found. Install from https://go.dev/dl/" + exit 1 +fi + +# --- Kernel config check --- +echo "" +echo "๐Ÿ” Checking kernel eBPF support..." +if [[ -d /sys/kernel/debug/tracing ]]; then + echo " โœ“ debugfs mounted" +else + echo " โš  debugfs not mounted. Run: sudo mount -t debugfs debugfs /sys/kernel/debug" +fi + +if grep -q CONFIG_BPF=y /boot/config-$(uname -r) 2>/dev/null; then + echo " โœ“ CONFIG_BPF enabled" +else + echo " โš  Could not verify CONFIG_BPF (may still work)" +fi + +# --- Network tuning (optional) --- +echo "" +echo "๐Ÿ”ง Applying network tuning..." +sudo sysctl -w net.core.rmem_max=26214400 2>/dev/null && echo " โœ“ rmem_max=25MB" || true +sudo sysctl -w net.core.wmem_max=26214400 2>/dev/null && echo " โœ“ wmem_max=25MB" || true +sudo sysctl -w net.core.rmem_default=1048576 2>/dev/null || true +sudo sysctl -w net.core.wmem_default=1048576 2>/dev/null || true + +echo "" +echo "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" +echo "โ•‘ โœ“ Setup complete! โ•‘" +echo "โ•‘ โ•‘" +echo "โ•‘ Quick start: โ•‘" +echo "โ•‘ python3 run_experiment.py --duration 10 โ•‘" +echo "โ•‘ โ•‘" +echo "โ•‘ With eBPF (needs sudo): โ•‘" +echo "โ•‘ python3 run_experiment.py --ebpf โ•‘" +echo "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..420be8c --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module mpquic-exp + +go 1.26.3 + +require github.com/AeonDave/mp-quic-go v0.1.3 + +require ( + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0279849 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/AeonDave/mp-quic-go v0.1.3 h1:iJUJ1+RDNQjWBljk0w34u0fGihzNoyJmMZoJSaIqiNg= +github.com/AeonDave/mp-quic-go v0.1.3/go.mod h1:uJ/V2CfzOuu6YpagbsR58dk+CSKvWsGsRi/Kcwqh+Dk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/ebpf/measure.py b/server/ebpf/measure.py new file mode 100644 index 0000000..5558a17 --- /dev/null +++ b/server/ebpf/measure.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""eBPF-based UDP tracer for MP-QUIC performance measurement. + +Traces kernel-level UDP send/receive latency and packet drops. +Requires root privileges and Linux with BCC installed. +""" + +from bcc import BPF +import time +import argparse +import signal +import sys + +bpf_text = """ +#include +#include +#include +#include + +#define MPQUIC_PORT __TARGET_PORT__ + +BPF_HASH(send_start, u32, u64); +BPF_HASH(recv_start, u32, u64); + +BPF_PERF_OUTPUT(events); + +struct event_t { + u64 timestamp_ns; + u64 delta_ns; + u32 pid; + u16 sport; + u16 dport; + u8 event_type; // 0=SEND, 1=RECV, 2=DROP +}; + +int trace_udp_sendmsg(struct pt_regs *ctx, struct sock *sk) { + struct inet_sock *inet = (struct inet_sock *)sk; + u16 dport = 0; + bpf_probe_read_kernel(&dport, sizeof(dport), &inet->inet_dport); + dport = ntohs(dport); + + // Filter: only trace traffic to/from MP-QUIC port + u16 sport = 0; + bpf_probe_read_kernel(&sport, sizeof(sport), &inet->inet_sport); + sport = ntohs(sport); + + if (dport != MPQUIC_PORT && sport != MPQUIC_PORT) + return 0; + + u32 pid = bpf_get_current_pid_tgid(); + u64 ts = bpf_ktime_get_ns(); + send_start.update(&pid, &ts); + return 0; +} + +int trace_udp_sendmsg_ret(struct pt_regs *ctx) { + u32 pid = bpf_get_current_pid_tgid(); + u64 *tsp = send_start.lookup(&pid); + if (tsp == 0) + return 0; + + u64 delta = bpf_ktime_get_ns() - *tsp; + send_start.delete(&pid); + + struct event_t event = {}; + event.timestamp_ns = bpf_ktime_get_ns(); + event.delta_ns = delta; + event.pid = pid; + event.event_type = 0; // SEND + events.perf_submit(ctx, &event, sizeof(event)); + return 0; +} + +int trace_udp_recvmsg(struct pt_regs *ctx, struct sock *sk) { + struct inet_sock *inet = (struct inet_sock *)sk; + u16 sport = 0; + bpf_probe_read_kernel(&sport, sizeof(sport), &inet->inet_sport); + sport = ntohs(sport); + + u16 dport = 0; + bpf_probe_read_kernel(&dport, sizeof(dport), &inet->inet_dport); + dport = ntohs(dport); + + if (sport != MPQUIC_PORT && dport != MPQUIC_PORT) + return 0; + + u32 pid = bpf_get_current_pid_tgid(); + u64 ts = bpf_ktime_get_ns(); + recv_start.update(&pid, &ts); + return 0; +} + +int trace_udp_recvmsg_ret(struct pt_regs *ctx) { + u32 pid = bpf_get_current_pid_tgid(); + u64 *tsp = recv_start.lookup(&pid); + if (tsp == 0) + return 0; + + u64 delta = bpf_ktime_get_ns() - *tsp; + recv_start.delete(&pid); + + struct event_t event = {}; + event.timestamp_ns = bpf_ktime_get_ns(); + event.delta_ns = delta; + event.pid = pid; + event.event_type = 1; // RECV + events.perf_submit(ctx, &event, sizeof(event)); + return 0; +} + +TRACEPOINT_PROBE(skb, kfree_skb) { + struct event_t event = {}; + event.timestamp_ns = bpf_ktime_get_ns(); + event.delta_ns = 0; + event.pid = bpf_get_current_pid_tgid(); + event.event_type = 2; // DROP + events.perf_submit(ctx, &event, sizeof(event)); + return 0; +} +""" + +EVENT_TYPES = {0: "SEND_LATENCY", 1: "RECV_LATENCY", 2: "DROP"} + +# Counters for live summary +stats = {"send_count": 0, "recv_count": 0, "drop_count": 0, + "send_total_ns": 0, "recv_total_ns": 0} + + +def main(): + parser = argparse.ArgumentParser(description="eBPF UDP Tracer for MP-QUIC") + parser.add_argument("--output", type=str, default="measurement.csv", + help="CSV output file") + parser.add_argument("--port", type=int, default=4242, + help="MP-QUIC port to filter on") + args = parser.parse_args() + + program = bpf_text.replace("__TARGET_PORT__", str(args.port)) + + print(f"Compiling eBPF program (requires root)...") + print(f"Filtering on UDP port {args.port}") + b = BPF(text=program) + + # Attach kprobes for send and receive paths + b.attach_kprobe(event="udp_sendmsg", fn_name="trace_udp_sendmsg") + b.attach_kretprobe(event="udp_sendmsg", fn_name="trace_udp_sendmsg_ret") + b.attach_kprobe(event="udp_recvmsg", fn_name="trace_udp_recvmsg") + b.attach_kretprobe(event="udp_recvmsg", fn_name="trace_udp_recvmsg_ret") + + csv_file = open(args.output, "w") + csv_file.write("timestamp,event_type,value_ns,pid\n") + + def handle_event(cpu, data, size): + event = b["events"].event(data) + etype = EVENT_TYPES.get(event.event_type, "UNKNOWN") + csv_file.write(f"{time.time()},{etype},{event.delta_ns},{event.pid}\n") + + # Update live stats + if event.event_type == 0: + stats["send_count"] += 1 + stats["send_total_ns"] += event.delta_ns + elif event.event_type == 1: + stats["recv_count"] += 1 + stats["recv_total_ns"] += event.delta_ns + elif event.event_type == 2: + stats["drop_count"] += 1 + + b["events"].open_perf_buffer(handle_event) + + def signal_handler(sig, frame): + print("\n--- Measurement Summary ---") + if stats["send_count"] > 0: + avg_send = stats["send_total_ns"] / stats["send_count"] / 1000 + print(f" Send events: {stats['send_count']:>8} (avg {avg_send:.1f} ยตs)") + if stats["recv_count"] > 0: + avg_recv = stats["recv_total_ns"] / stats["recv_count"] / 1000 + print(f" Recv events: {stats['recv_count']:>8} (avg {avg_recv:.1f} ยตs)") + print(f" Drop events: {stats['drop_count']:>8}") + print(f" Output file: {args.output}") + csv_file.close() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + print(f"Tracing... Output โ†’ {args.output}. Ctrl-C to stop.") + while True: + b.perf_buffer_poll() + + +if __name__ == "__main__": + main() diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..c085e45 --- /dev/null +++ b/server/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/binary" + "encoding/csv" + "encoding/pem" + "flag" + "fmt" + "io" + "log" + "math/big" + "os" + "strconv" + "sync" + "time" + + quic "github.com/AeonDave/mp-quic-go" +) + +func generateTLSConfig() *tls.Config { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{"mpquic-exp"}, + } +} + +func main() { + addr := flag.String("addr", "0.0.0.0:4242", "Address to listen on") + maxPaths := flag.Int("maxpaths", 4, "Maximum number of paths") + outCSV := flag.String("output", "app_metrics.csv", "Output CSV for application-level metrics") + flag.Parse() + + listener, err := quic.ListenAddr(*addr, generateTLSConfig(), &quic.Config{ + MaxPaths: *maxPaths, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Server listening on %s with max %d paths\n", *addr, *maxPaths) + fmt.Printf("Logging app-level metrics to %s\n", *outCSV) + + // Open CSV + f, err := os.OpenFile(*outCSV, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + log.Fatal("Failed to open CSV:", err) + } + defer f.Close() + + writer := csv.NewWriter(f) + writer.Write([]string{"sequence_number", "drone_send_time", "ground_recv_time", "latency_ns", "bytes_received"}) + writer.Flush() + + // Mutex to protect CSV writer if multiple streams are used + var mu sync.Mutex + + for { + conn, err := listener.Accept(context.Background()) + if err != nil { + log.Fatal(err) + } + fmt.Println("Client connected:", conn.RemoteAddr()) + + go func(conn *quic.Conn) { + for { + stream, err := conn.AcceptStream(context.Background()) + if err != nil { + fmt.Println("Session error:", err) + return + } + go func(stream *quic.Stream) { + header := make([]byte, 4) + for { + // Read 4-byte length prefix + _, err := io.ReadFull(stream, header) + if err != nil { + fmt.Printf("Stream closed\n") + return + } + + length := binary.BigEndian.Uint32(header) + if length < 20 { + fmt.Printf("Warning: Invalid frame length %d\n", length) + continue + } + + // Read rest of the payload + payload := make([]byte, length-4) + _, err = io.ReadFull(stream, payload) + if err != nil { + fmt.Printf("Stream read error: %v\n", err) + return + } + + recvTime := time.Now().UnixNano() + + seqNum := binary.BigEndian.Uint64(payload[0:8]) + sendTime := binary.BigEndian.Uint64(payload[8:16]) + latency := recvTime - int64(sendTime) + + mu.Lock() + writer.Write([]string{ + strconv.FormatUint(seqNum, 10), + strconv.FormatUint(sendTime, 10), + strconv.FormatInt(recvTime, 10), + strconv.FormatInt(latency, 10), + strconv.FormatUint(uint64(length), 10), + }) + // Periodically flush? + if seqNum % 1000 == 0 { + writer.Flush() + } + mu.Unlock() + } + }(stream) + } + }(conn) + } +} diff --git a/server/scripts/run.sh b/server/scripts/run.sh new file mode 100755 index 0000000..5f0bdb2 --- /dev/null +++ b/server/scripts/run.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Build the server +echo "Building server..." +go build -o server_bin main.go + +echo "Starting server..." +./server_bin "$@" diff --git a/server/scripts/setup.sh b/server/scripts/setup.sh new file mode 100644 index 0000000..e570bad --- /dev/null +++ b/server/scripts/setup.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Setup script for MP-QUIC experiment environment. +# Run on each Linux machine (server + client) before experiments. +set -euo pipefail + +echo "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" +echo "โ•‘ MP-QUIC Experiment Environment Setup โ•‘" +echo "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + +# --- OS Check --- +if [[ ! -f /etc/os-release ]]; then + echo "โŒ Not running on Linux. eBPF tracing requires Linux." + echo " You can still build and run the Go binaries on this machine." + exit 1 +fi + +. /etc/os-release +echo "๐Ÿ“‹ Detected OS: $PRETTY_NAME" + +# --- Install dependencies --- +if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then + echo "" + echo "๐Ÿ“ฆ Installing packages..." + sudo apt-get update -qq + sudo apt-get install -y -qq \ + bpfcc-tools \ + python3-bpfcc \ + linux-headers-$(uname -r) \ + golang-go \ + iperf3 \ + net-tools \ + iproute2 + + # Ensure Python3 BCC bindings work + python3 -c "from bcc import BPF; print(' โœ“ BCC Python bindings OK')" 2>/dev/null || { + echo " โš  BCC Python import failed. Try: sudo apt install python3-bpfcc" + } +elif [[ "$ID" == "fedora" || "$ID" == "rhel" || "$ID" == "centos" ]]; then + echo "" + echo "๐Ÿ“ฆ Installing packages (DNF)..." + sudo dnf install -y \ + bcc-tools \ + python3-bcc \ + kernel-devel-$(uname -r) \ + golang \ + iperf3 \ + iproute +else + echo "โš  Unsupported distro: $ID" + echo " Please install manually: bcc-tools, python3-bcc, golang, linux-headers" +fi + +# --- Verify Go --- +echo "" +if command -v go &>/dev/null; then + echo "โœ“ Go $(go version | awk '{print $3}')" +else + echo "โŒ Go not found. Install from https://go.dev/dl/" + exit 1 +fi + +# --- Kernel config check --- +echo "" +echo "๐Ÿ” Checking kernel eBPF support..." +if [[ -d /sys/kernel/debug/tracing ]]; then + echo " โœ“ debugfs mounted" +else + echo " โš  debugfs not mounted. Run: sudo mount -t debugfs debugfs /sys/kernel/debug" +fi + +if grep -q CONFIG_BPF=y /boot/config-$(uname -r) 2>/dev/null; then + echo " โœ“ CONFIG_BPF enabled" +else + echo " โš  Could not verify CONFIG_BPF (may still work)" +fi + +# --- Network tuning (optional) --- +echo "" +echo "๐Ÿ”ง Applying network tuning..." +sudo sysctl -w net.core.rmem_max=26214400 2>/dev/null && echo " โœ“ rmem_max=25MB" || true +sudo sysctl -w net.core.wmem_max=26214400 2>/dev/null && echo " โœ“ wmem_max=25MB" || true +sudo sysctl -w net.core.rmem_default=1048576 2>/dev/null || true +sudo sysctl -w net.core.wmem_default=1048576 2>/dev/null || true + +echo "" +echo "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—" +echo "โ•‘ โœ“ Setup complete! โ•‘" +echo "โ•‘ โ•‘" +echo "โ•‘ Quick start: โ•‘" +echo "โ•‘ python3 run_experiment.py --duration 10 โ•‘" +echo "โ•‘ โ•‘" +echo "โ•‘ With eBPF (needs sudo): โ•‘" +echo "โ•‘ python3 run_experiment.py --ebpf โ•‘" +echo "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"