first
This commit is contained in:
@@ -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 <GROUND_STATION_IP>: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!
|
||||||
Binary file not shown.
Executable
+144
@@ -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()
|
||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -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 <uapi/linux/ptrace.h>
|
||||||
|
#include <net/sock.h>
|
||||||
|
#include <net/inet_sock.h>
|
||||||
|
#include <bcc/proto.h>
|
||||||
|
|
||||||
|
#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()
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import quic "github.com/AeonDave/mp-quic-go"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("lowlatency", func() quic.PathScheduler {
|
||||||
|
return quic.NewLowLatencyScheduler()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package scheduler
|
||||||
|
|
||||||
|
import quic "github.com/AeonDave/mp-quic-go"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("roundrobin", func() quic.PathScheduler {
|
||||||
|
return quic.NewRoundRobinScheduler()
|
||||||
|
})
|
||||||
|
}
|
||||||
Executable
+11
@@ -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 "$@"
|
||||||
@@ -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 "╚═══════════════════════════════════════════╝"
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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 <uapi/linux/ptrace.h>
|
||||||
|
#include <net/sock.h>
|
||||||
|
#include <net/inet_sock.h>
|
||||||
|
#include <bcc/proto.h>
|
||||||
|
|
||||||
|
#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()
|
||||||
+139
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+11
@@ -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 "$@"
|
||||||
@@ -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 "╚═══════════════════════════════════════════╝"
|
||||||
Reference in New Issue
Block a user