chore: import upstream snapshot with attribution
Rust / build (push) Failing after 1s
Rust / docker (push) Failing after 0s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:29:44 +08:00
commit 19dc5d82a0
236 changed files with 224662 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# Script to empty the logs directory except for the last (most recent) log file.
LOG_DIR="logs"
# Check if the log directory exists
if [ ! -d "$LOG_DIR" ]; then
echo "Log directory '$LOG_DIR' not found."
exit 1
fi
# Change to the log directory to make file operations simpler
cd "$LOG_DIR" || exit 1
# Count the number of files in the directory
# Using find to correctly handle filenames with spaces or special characters
# and to only count regular files (-type f) directly in this directory (-maxdepth 1)
num_files=$(find . -maxdepth 1 -type f -print | wc -l)
if [ "$num_files" -le 1 ]; then
echo "No old log files to remove (found $num_files file(s))."
# Change back to the original directory before exiting
cd - > /dev/null
exit 0
fi
# List all files, sort by modification time (newest first), then skip the first one (the newest)
# and pass the rest to xargs for deletion.
# `ls -t` sorts by modification time.
# `tail -n +2` skips the first line.
# `xargs -d '\n'` handles filenames with spaces.
files_to_delete=$(ls -t | tail -n +2)
if [ -n "$files_to_delete" ]; then
echo "Removing the following old log files:"
# Loop through files to print them before deleting
echo "$files_to_delete" | while IFS= read -r file_to_delete; do
echo " - $file_to_delete"
done
# Actual deletion
for file_to_delete in $files_to_delete; do
# Check if the file exists before trying to delete it
if [ -e "$file_to_delete" ]; then
rm -f "$file_to_delete"
else
echo "Warning: File '$file_to_delete' does not exist."
fi
done
# Count how many were actually passed to xargs (can be tricky if names have newlines)
# A simpler way is to count lines from the `files_to_delete` variable
num_deleted=$(echo "$files_to_delete" | wc -l)
echo "Successfully removed $num_deleted old log file(s)."
else
echo "No old log files to remove."
fi
# Change back to the original directory
cd - > /dev/null
exit 0
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -uo pipefail
# Debug harness for the short-lived-process attribution race.
#
# Runs rustnet with debug logging IN THE FOREGROUND for ~20s (you will
# see the TUI take over; it exits by itself), while a background loop
# fires short-lived curl/dig processes. Captures the eBPF bpf_printk
# stream from the kernel tracing pipe, then summarizes what the
# process-lookup pipeline did for exactly those connections.
#
# Usage: scripts/debug-attribution.sh (will sudo)
OUT_DIR="/tmp/claude/attribution-debug"
mkdir -p "${OUT_DIR}"
rm -f "${OUT_DIR}"/*.log 2>/dev/null
cd "$(dirname "$0")/.."
sudo -v || exit 1
# tracefs location differs across distros.
TRACE_PIPE="/sys/kernel/tracing/trace_pipe"
[[ -e "${TRACE_PIPE}" ]] || TRACE_PIPE="/sys/kernel/debug/tracing/trace_pipe"
# Capture bpf_printk output (e.g. "map update failed") while the test runs.
sudo sh -c "timeout 24 cat ${TRACE_PIPE}" > "${OUT_DIR}/bpf_trace.log" 2>"${OUT_DIR}/bpf_trace.err" &
# Background traffic: short-lived processes, started after capture warms up.
(
sleep 6
for i in 1 2 3; do
curl -s -o /dev/null --max-time 3 https://example.com
echo "curl #$i done ($(date +%H:%M:%S.%3N))" >> "${OUT_DIR}/traffic.log"
dig +short +time=1 +tries=1 "example.com" @1.1.1.1 >/dev/null 2>&1
echo "dig #$i done ($(date +%H:%M:%S.%3N))" >> "${OUT_DIR}/traffic.log"
sleep 2
done
) &
TRAFFIC_PID=$!
echo "Starting rustnet for 20s (TUI will take over this terminal)..."
sleep 1
sudo sh -c 'timeout 20 target/release/rustnet -l debug' || true
# rustnet may have been killed mid-frame; restore the terminal.
stty sane 2>/dev/null
printf '\033[?1049l\033[?25h\033[?1000l\033[?1006l\n'
wait ${TRAFFIC_PID} 2>/dev/null
# The logs/ dir is created 0700 by the (privilege-dropped) rustnet
# process, so globbing it needs sudo.
LOG="$(sudo sh -c 'ls -t logs/rustnet_*.log 2>/dev/null' | head -1)"
if [[ -z "${LOG}" ]]; then
echo "ERROR: no rustnet log produced; see ${OUT_DIR}/"
exit 1
fi
sudo cp "${LOG}" "${OUT_DIR}/rustnet-debug.log"
sudo chown "$(id -u)" "${OUT_DIR}/rustnet-debug.log"
LOG="${OUT_DIR}/rustnet-debug.log"
echo ""
echo "=== Summary (${LOG}) ==="
echo "-- lookup outcome counts --"
echo " eBPF hits: $(grep -c 'Enhanced lookup: eBPF hit' "${LOG}")"
echo " eBPF misses: $(grep -c 'Enhanced lookup: eBPF miss' "${LOG}")"
echo " zero-source rescues: $(grep -c 'succeeded with zero source' "${LOG}")"
echo " procfs name sets: $(grep -c 'Set process name' "${LOG}")"
echo ""
echo "-- :443 (curl) lookup trace (first 30) --"
grep -E "Trying eBPF|eBPF lookup (successful|missed)|Set process name" "${LOG}" | grep ":443" | head -30
echo ""
echo "-- 1.1.1.1 (dig) lookup trace (first 20) --"
grep -E "Trying eBPF|eBPF lookup (successful|missed)|Set process name" "${LOG}" | grep "1\.1\.1\.1" | head -20
echo ""
echo "-- bpf_printk (kernel side) --"
grep -cE "map update failed" "${OUT_DIR}/bpf_trace.log" | sed 's/^/ map update failures: /'
wc -l < "${OUT_DIR}/bpf_trace.log" | sed 's/^/ total trace lines: /'
echo ""
echo "-- traffic timeline --"
cat "${OUT_DIR}/traffic.log" 2>/dev/null
echo ""
echo "Full logs preserved in ${OUT_DIR}/"
+424
View File
@@ -0,0 +1,424 @@
#!/usr/bin/env python3
"""
Enrich RustNet PCAP captures with process and GeoIP information from sidecar JSONL.
This script correlates packets in a PCAP file with process and GeoIP information
from the accompanying .connections.jsonl file created by RustNet.
NOTE: If you captured using PKTAP on macOS (e.g., `--interface pktap,en0`),
the process information is already embedded in the PCAP file itself.
You can view it directly in Wireshark without using this script.
This script is only needed for regular (non-PKTAP) captures.
Usage:
# Show packets with process info
python pcap_enrich.py capture.pcap
# Export to annotated PCAPNG (requires editcap from Wireshark)
python pcap_enrich.py capture.pcap --output annotated.pcapng
# Generate TSV report
python pcap_enrich.py capture.pcap --format tsv > report.tsv
Requirements:
pip install scapy
"""
import argparse
import json
import subprocess
import sys
import tempfile
from pathlib import Path
try:
from scapy.all import rdpcap, IP, TCP, UDP, ICMP
except ImportError:
print("Error: scapy is required. Install with: pip install scapy", file=sys.stderr)
sys.exit(1)
def parse_systemtime(st) -> float | None:
"""Parse a SystemTime serialized as {secs_since_epoch, nanos_since_epoch}."""
if st is None:
return None
if isinstance(st, dict):
secs = st.get("secs_since_epoch", 0)
nanos = st.get("nanos_since_epoch", 0)
return secs + nanos / 1e9
# Fallback for other formats
return None
def load_connections(jsonl_path: Path) -> dict:
"""Load connection-to-process mappings from JSONL file.
Returns a dict mapping (proto, local, remote) -> list of connection info dicts.
Multiple connections can exist for the same tuple (port reuse over time).
"""
lookup = {}
if not jsonl_path.exists():
print(f"Warning: Sidecar file not found: {jsonl_path}", file=sys.stderr)
return lookup
with open(jsonl_path) as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
c = json.loads(line)
proto = c.get("protocol", "").upper()
local = c.get("local_addr", "")
remote = c.get("remote_addr", "")
if proto and local and remote:
info = {
"pid": c.get("pid"),
"process_name": c.get("process_name"),
"first_seen": parse_systemtime(c.get("first_seen")),
"last_seen": parse_systemtime(c.get("last_seen")),
"bytes_sent": c.get("bytes_sent", 0),
"bytes_received": c.get("bytes_received", 0),
# GeoIP information
"geoip_country_code": c.get("geoip_country_code"),
"geoip_country_name": c.get("geoip_country_name"),
"geoip_asn": c.get("geoip_asn"),
"geoip_as_org": c.get("geoip_as_org"),
"geoip_city": c.get("geoip_city"),
"geoip_postal_code": c.get("geoip_postal_code"),
}
# Store both directions, as a list to handle port reuse
for key in [(proto, local, remote), (proto, remote, local)]:
if key not in lookup:
lookup[key] = []
lookup[key].append(info)
except json.JSONDecodeError as e:
print(f"Warning: Invalid JSON at line {line_num}: {e}", file=sys.stderr)
return lookup
def find_matching_connection(lookup: dict, pkt_tuple: tuple, pkt_time: float, slack: float) -> dict | None:
"""Find the best matching connection for a packet based on tuple and timestamp.
Args:
lookup: Connection lookup dict
pkt_tuple: (proto, src, dst) tuple from packet
pkt_time: Packet timestamp (seconds since epoch)
slack: Allowed time slack in seconds
Returns:
Best matching connection info dict, or None if no match
"""
connections = lookup.get(pkt_tuple, [])
if not connections:
return None
best_match = None
best_score = float('inf')
for conn in connections:
first_seen = conn.get("first_seen")
last_seen = conn.get("last_seen")
# If no timestamps, fall back to simple match (first connection wins)
if first_seen is None or last_seen is None:
if best_match is None:
best_match = conn
continue
# Check if packet falls within connection time range (with slack)
if first_seen - slack <= pkt_time <= last_seen + slack:
# Score by how close the packet is to the connection's time range
# Prefer connections where the packet is well within the range
if pkt_time < first_seen:
score = first_seen - pkt_time
elif pkt_time > last_seen:
score = pkt_time - last_seen
else:
score = 0 # Perfect match (within range)
if score < best_score:
best_score = score
best_match = conn
return best_match
def get_packet_tuple(pkt) -> tuple:
"""Extract connection tuple from packet."""
if not pkt.haslayer(IP):
return None
ip = pkt[IP]
src_ip = ip.src
dst_ip = ip.dst
if pkt.haslayer(TCP):
tcp = pkt[TCP]
return ("TCP", f"{src_ip}:{tcp.sport}", f"{dst_ip}:{tcp.dport}")
elif pkt.haslayer(UDP):
udp = pkt[UDP]
return ("UDP", f"{src_ip}:{udp.sport}", f"{dst_ip}:{udp.dport}")
elif pkt.haslayer(ICMP):
return ("ICMP", src_ip, dst_ip)
return None
def enrich_packets(pcap_path: Path, lookup: dict, slack: float):
"""Yield enriched packet information."""
packets = rdpcap(str(pcap_path))
for frame_num, pkt in enumerate(packets, 1):
pkt_tuple = get_packet_tuple(pkt)
pkt_time = float(pkt.time)
if not pkt_tuple:
yield {
"frame": frame_num,
"time": pkt_time,
"proto": "OTHER",
"src": "",
"dst": "",
"pid": None,
"process": None,
"country": None,
"city": None,
"postal_code": None,
"asn": None,
"as_org": None,
}
continue
proto, src, dst = pkt_tuple
info = find_matching_connection(lookup, pkt_tuple, pkt_time, slack) or {}
yield {
"frame": frame_num,
"time": pkt_time,
"proto": proto,
"src": src,
"dst": dst,
"pid": info.get("pid"),
"process": info.get("process_name"),
"bytes_sent": info.get("bytes_sent"),
"bytes_received": info.get("bytes_received"),
"country": info.get("geoip_country_code"),
"city": info.get("geoip_city"),
"postal_code": info.get("geoip_postal_code"),
"asn": info.get("geoip_asn"),
"as_org": info.get("geoip_as_org"),
}
def print_table(packets: list):
"""Print enriched packets as a formatted table."""
print(f"{'Frame':>6} {'Proto':<5} {'Source':<22} {'Destination':<22} {'Loc':<4} {'City':<15} {'ASN':>8} {'PID':>7} {'Process':<15}")
print("-" * 116)
for p in packets:
pid_str = str(p["pid"]) if p["pid"] else "-"
proc_str = p["process"] or "-"
if len(proc_str) > 15:
proc_str = proc_str[:12] + "..."
loc_str = p["country"] or "-"
city_str = p["city"] or "-"
if len(city_str) > 15:
city_str = city_str[:12] + "..."
asn_str = f"AS{p['asn']}" if p["asn"] else "-"
src_str = p["src"][:22] if len(p["src"]) > 22 else p["src"]
dst_str = p["dst"][:22] if len(p["dst"]) > 22 else p["dst"]
print(f"{p['frame']:>6} {p['proto']:<5} {src_str:<22} {dst_str:<22} {loc_str:<4} {city_str:<15} {asn_str:>8} {pid_str:>7} {proc_str:<15}")
def print_tsv(packets: list):
"""Print enriched packets as TSV."""
print("frame\ttime\tproto\tsrc\tdst\tcountry\tcity\tpostal_code\tasn\tas_org\tpid\tprocess")
for p in packets:
print(f"{p['frame']}\t{p['time']:.6f}\t{p['proto']}\t{p['src']}\t{p['dst']}\t{p['country'] or ''}\t{p['city'] or ''}\t{p['postal_code'] or ''}\t{p['asn'] or ''}\t{p['as_org'] or ''}\t{p['pid'] or ''}\t{p['process'] or ''}")
def print_json(packets: list):
"""Print enriched packets as JSON."""
print(json.dumps(packets, indent=2))
def create_pcapng(pcap_path: Path, packets: list, output_path: Path):
"""Create annotated PCAPNG using editcap."""
# Check if editcap is available
try:
subprocess.run(["editcap", "--version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: editcap not found. Install Wireshark to get editcap.", file=sys.stderr)
sys.exit(1)
# First convert to pcapng
with tempfile.NamedTemporaryFile(suffix=".pcapng", delete=False) as tmp:
tmp_path = Path(tmp.name)
subprocess.run(["editcap", "-F", "pcapng", str(pcap_path), str(tmp_path)], check=True)
# Build annotation commands
# editcap -a "frame:comment" format
annotations = []
for p in packets:
if p["pid"] or p["process"] or p["country"] or p["city"] or p["postal_code"] or p["asn"]:
comment_parts = []
if p["country"]:
comment_parts.append(f"Loc:{p['country']}")
if p["city"]:
comment_parts.append(f"City:{p['city']}")
if p["postal_code"]:
comment_parts.append(f"ZIP:{p['postal_code']}")
if p["asn"]:
comment_parts.append(f"AS{p['asn']}")
if p["pid"]:
comment_parts.append(f"PID:{p['pid']}")
if p["process"]:
comment_parts.append(f"Process:{p['process']}")
comment = " ".join(comment_parts)
annotations.append(f"{p['frame']}:{comment}")
if not annotations:
print("No process or GeoIP information found to annotate.", file=sys.stderr)
# Just copy the pcapng as-is
tmp_path.rename(output_path)
return
# Apply annotations in batches (editcap has command line limits)
current_input = tmp_path
batch_size = 100
for i in range(0, len(annotations), batch_size):
batch = annotations[i:i + batch_size]
with tempfile.NamedTemporaryFile(suffix=".pcapng", delete=False) as tmp2:
tmp2_path = Path(tmp2.name)
cmd = ["editcap"]
for ann in batch:
cmd.extend(["-a", ann])
cmd.extend([str(current_input), str(tmp2_path)])
subprocess.run(cmd, check=True)
if current_input != tmp_path:
current_input.unlink()
current_input = tmp2_path
# Move final result to output
current_input.rename(output_path)
if tmp_path.exists():
tmp_path.unlink()
print(f"Created annotated PCAPNG: {output_path}")
print(f"Annotated {len(annotations)} packets with process/GeoIP information.")
def count_unique_connections(lookup: dict) -> int:
"""Count unique connections (accounting for bidirectional storage)."""
seen = set()
count = 0
for key, conns in lookup.items():
for conn in conns:
# Create a unique identifier for each connection
conn_id = (key, conn.get("first_seen"), conn.get("pid"))
if conn_id not in seen:
seen.add(conn_id)
count += 1
return count // 2 # Divide by 2 because we store both directions
def print_summary(packets: list, lookup: dict):
"""Print a summary of process information found."""
total = len(packets)
with_pid = sum(1 for p in packets if p["pid"])
# Group by process
by_process = {}
for p in packets:
proc = p["process"] or "<unknown>"
if proc not in by_process:
by_process[proc] = {"count": 0, "pid": p["pid"]}
by_process[proc]["count"] += 1
print(f"\nSummary:")
print(f" Total packets: {total}")
print(f" Packets with process info: {with_pid} ({100*with_pid/total:.1f}%)")
print(f" Unique connections in sidecar: {count_unique_connections(lookup)}")
print(f"\nPackets by process:")
for proc, info in sorted(by_process.items(), key=lambda x: -x[1]["count"]):
pid_str = f" (PID {info['pid']})" if info["pid"] else ""
print(f" {proc}{pid_str}: {info['count']} packets")
def main():
parser = argparse.ArgumentParser(
description="Enrich RustNet PCAP captures with process and GeoIP information.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s capture.pcap # Show packets with process info
%(prog)s capture.pcap --format tsv # Output as TSV
%(prog)s capture.pcap --format json # Output as JSON
%(prog)s capture.pcap -o annotated.pcapng # Create annotated PCAPNG
%(prog)s capture.pcap --summary # Show summary only
%(prog)s capture.pcap --slack 5 # Use 5 second slack for timestamp matching
"""
)
parser.add_argument("pcap", type=Path, help="Path to PCAP file")
parser.add_argument("-j", "--jsonl", type=Path,
help="Path to sidecar JSONL file (default: <pcap>.connections.jsonl)")
parser.add_argument("-o", "--output", type=Path,
help="Output annotated PCAPNG file")
parser.add_argument("-f", "--format", choices=["table", "tsv", "json"], default="table",
help="Output format (default: table)")
parser.add_argument("-s", "--summary", action="store_true",
help="Show summary only")
parser.add_argument("-l", "--limit", type=int, default=0,
help="Limit number of packets to process (0 = no limit)")
parser.add_argument("--slack", type=float, default=2.0,
help="Timestamp matching slack in seconds (default: 2.0)")
args = parser.parse_args()
if not args.pcap.exists():
print(f"Error: PCAP file not found: {args.pcap}", file=sys.stderr)
sys.exit(1)
# Default sidecar path
jsonl_path = args.jsonl or Path(f"{args.pcap}.connections.jsonl")
# Load connection mappings
lookup = load_connections(jsonl_path)
if lookup:
print(f"Loaded {count_unique_connections(lookup)} connections from {jsonl_path}", file=sys.stderr)
# Process packets
packets = list(enrich_packets(args.pcap, lookup, args.slack))
if args.limit > 0:
packets = packets[:args.limit]
if args.summary:
print_summary(packets, lookup)
return
if args.output:
create_pcapng(args.pcap, packets, args.output)
print_summary(packets, lookup)
else:
if args.format == "table":
print_table(packets)
print_summary(packets, lookup)
elif args.format == "tsv":
print_tsv(packets)
elif args.format == "json":
print_json(packets)
if __name__ == "__main__":
main()
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
# Pre-release validation script for RustNet.
# Run this before tagging a release to catch common issues.
#
# Usage: ./scripts/pre-release-check.sh [version]
# e.g.: ./scripts/pre-release-check.sh 1.2.0
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ERRORS=0
WARNINGS=0
pass() { echo -e " ${GREEN}${NC} $1"; }
fail() { echo -e " ${RED}${NC} $1"; ERRORS=$((ERRORS + 1)); }
warn() { echo -e " ${YELLOW}!${NC} $1"; WARNINGS=$((WARNINGS + 1)); }
VERSION="${1:-}"
echo "RustNet Pre-Release Checks"
echo "=========================="
echo
# --- Version consistency ---
echo "Version consistency:"
# Read the binary's [package] version, NOT the [workspace.package] version.
# Since the workspace split, Cargo.toml has two `version =` lines and the
# workspace one (0.x libraries) comes first, so `grep '^version' | head -1`
# would read the wrong value. Anchor on the [package] section instead.
CARGO_VERSION=$(awk -F'"' '/^\[package\]/{p=1} p && /^version = /{print $2; exit}' Cargo.toml)
RPM_VERSION=$(grep '^Version:' rpm/rustnet.spec | awk '{print $2}')
if [ -n "$VERSION" ]; then
if [ "$CARGO_VERSION" = "$VERSION" ]; then
pass "Cargo.toml version: $CARGO_VERSION"
else
fail "Cargo.toml version is $CARGO_VERSION, expected $VERSION"
fi
if [ "$RPM_VERSION" = "$VERSION" ]; then
pass "rpm/rustnet.spec version: $RPM_VERSION"
else
fail "rpm/rustnet.spec version is $RPM_VERSION, expected $VERSION"
fi
else
if [ "$CARGO_VERSION" = "$RPM_VERSION" ]; then
pass "Versions match: Cargo.toml=$CARGO_VERSION, rustnet.spec=$RPM_VERSION"
else
fail "Version mismatch: Cargo.toml=$CARGO_VERSION, rustnet.spec=$RPM_VERSION"
fi
VERSION="$CARGO_VERSION"
fi
echo
# --- Changelog ---
echo "Changelog:"
if grep -q "## \[$VERSION\]" CHANGELOG.md; then
pass "CHANGELOG.md has entry for $VERSION"
else
fail "CHANGELOG.md missing entry for [$VERSION]"
fi
if grep -q "\[$VERSION\]: https://github.com" CHANGELOG.md; then
pass "CHANGELOG.md has comparison link for $VERSION"
else
fail "CHANGELOG.md missing comparison link for $VERSION"
fi
UNRELEASED_CONTENT=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{found=0} found{print}' CHANGELOG.md | grep -v '^$' | head -1 || true)
if [ -z "$UNRELEASED_CONTENT" ]; then
pass "[Unreleased] section is empty (content moved to $VERSION)"
else
warn "[Unreleased] section still has content"
fi
echo
# --- Build checks ---
echo "Build checks:"
if cargo fmt --check > /dev/null 2>&1; then
pass "cargo fmt"
else
fail "cargo fmt --check has formatting issues"
fi
if cargo clippy -- -D warnings > /dev/null 2>&1; then
pass "cargo clippy"
else
fail "cargo clippy has warnings"
fi
if cargo test > /dev/null 2>&1; then
pass "cargo test"
else
fail "cargo test failed"
fi
echo
# --- Dockerfile ---
echo "Dockerfile:"
CARGO_BENCHES=$(grep -c '^\[\[bench\]\]' Cargo.toml || true)
if [ "$CARGO_BENCHES" -gt 0 ]; then
if grep -q 'COPY benches' Dockerfile; then
pass "Dockerfile copies benches/ directory"
else
fail "Cargo.toml defines $CARGO_BENCHES bench(es) but Dockerfile doesn't COPY benches/"
fi
fi
# Check that all include_bytes!()/include_str!() assets referenced at compile
# time are present in the Dockerfile's COPY commands. Since the workspace split,
# the baked-in assets (oui.gz, services) live under crates/rustnet-core/, not
# src/, and the include path is relative to the file doing the include — so scan
# both trees and resolve each path against its own source file's directory.
ROOT=$(pwd)
while IFS= read -r match; do
[ -z "$match" ] && continue
srcfile=${match%%:*} # path before the first ':'
asset=$(printf '%s\n' "$match" | sed -E 's/.*include_(bytes|str)!\("//; s/"\).*//')
srcdir=$(dirname "$srcfile")
# Resolve the include path (relative to srcfile) into a repo-root-relative path
# (portable, no GNU realpath needed).
resolved=$(cd "$srcdir" && python3 -c "import os.path,sys; print(os.path.relpath(os.path.abspath(sys.argv[1]), sys.argv[2]))" "$asset" "$ROOT" 2>/dev/null || echo "$asset")
if grep -q "$resolved\|$(basename "$resolved")" Dockerfile; then
pass "Dockerfile includes compile-time asset: $resolved"
else
fail "Compile-time asset $resolved not found in Dockerfile COPY commands"
fi
done < <(grep -rno -E 'include_(bytes|str)!\("[^"]*"\)' src/ crates/*/src 2>/dev/null | sort -u || true)
if docker info > /dev/null 2>&1; then
echo
echo "Docker build test:"
if docker build -t rustnet:pre-release-test . > /dev/null 2>&1; then
pass "Docker image builds successfully"
docker rmi rustnet:pre-release-test > /dev/null 2>&1 || true
else
fail "Docker build failed"
fi
else
warn "Docker not available, skipping Docker build test"
fi
echo
# --- Git status ---
echo "Git status:"
if git diff --quiet Cargo.lock; then
pass "Cargo.lock is up to date"
else
fail "Cargo.lock has uncommitted changes (run cargo build)"
fi
if [ -z "$(git status --porcelain -- src/ Cargo.toml Cargo.lock CHANGELOG.md rpm/)" ]; then
pass "No uncommitted changes in release files"
else
warn "Uncommitted changes in release files"
fi
echo
echo "=========================="
if [ "$ERRORS" -gt 0 ]; then
echo -e "${RED}$ERRORS error(s)${NC}, $WARNINGS warning(s) — fix errors before releasing"
exit 1
elif [ "$WARNINGS" -gt 0 ]; then
echo -e "${GREEN}0 errors${NC}, ${YELLOW}$WARNINGS warning(s)${NC} — review warnings before releasing"
else
echo -e "${GREEN}All checks passed!${NC} Ready to tag v$VERSION"
fi
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env bash
set -euo pipefail
# Automate the rustnet VHS recording: produces assets/rustnet.gif (demo)
# and assets/screenshots/*.png (README) in one pass.
#
# Prerequisites (Linux):
# vhs # required (https://github.com/charmbracelet/vhs)
# gifsicle # optional, for GIF size optimization
# cargo build --release # auto-run if target/release/rustnet missing
#
# Usage:
# scripts/record-rustnet-demo.sh
#
# What it does:
# 1. Verifies vhs and cargo are installed.
# 2. Builds target/release/rustnet if missing or stale vs Cargo.lock.
# 3. Grants the binary capture capabilities via `sudo setcap`, so the
# recording runs rustnet UNPRIVILEGED — the Security sidebar shows
# the sandboxed non-root posture instead of a root warning.
# 4. Spawns a background traffic generator (curl/dig/ping) so the
# connection table is busy and sparklines move.
# 5. Runs `vhs demo.tape` -> assets/rustnet.gif.
# 6. Runs `vhs screenshots.tape` -> assets/screenshots/*.png.
# 7. Optimizes the GIF with gifsicle -O3 --lossy=80 if available
# (terminal flat colors are unaffected by the lossy pass).
# 8. Cleans up the throwaway GIF and background processes.
# 9. Prints a summary of the produced assets.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
RUSTNET_DIR="$(dirname "$SCRIPT_DIR")"
DEMO_TAPE="${RUSTNET_DIR}/demo.tape"
SCREENSHOTS_TAPE="${RUSTNET_DIR}/screenshots.tape"
GIF_FILE="${RUSTNET_DIR}/assets/rustnet.gif"
SCREENSHOTS_DIR="${RUSTNET_DIR}/assets/screenshots"
THROWAWAY_GIF="${SCREENSHOTS_DIR}/_throwaway.gif"
RELEASE_BIN="${RUSTNET_DIR}/target/release/rustnet"
TRAFFIC_PID=""
# Background traffic generator: HTTPS, DNS, ICMP, multiple processes.
# Started fresh for each tape with a delay so that every generated
# connection is born AFTER the tape's rustnet instance (and its eBPF
# hooks) is up — connections whose process exits before rustnet starts
# can never be attributed and would show as dead "-" rows.
start_traffic() {
(
sleep 6
while true; do
curl -s -o /dev/null --max-time 4 https://example.com || true
curl -s -o /dev/null --max-time 4 https://github.com || true
curl -s -o /dev/null --max-time 4 https://wikipedia.org || true
curl -s -o /dev/null --max-time 4 https://ratatui.rs || true
dig +short +time=2 +tries=1 example.com >/dev/null 2>&1 || true
dig +short +time=2 +tries=1 github.com >/dev/null 2>&1 || true
ping -c 1 -W 1 1.1.1.1 >/dev/null 2>&1 || true
sleep 1
done
) &
TRAFFIC_PID=$!
}
stop_traffic() {
if [[ -n "${TRAFFIC_PID}" ]] && kill -0 "${TRAFFIC_PID}" 2>/dev/null; then
kill "${TRAFFIC_PID}" 2>/dev/null || true
wait "${TRAFFIC_PID}" 2>/dev/null || true
fi
TRAFFIC_PID=""
}
cleanup() {
stop_traffic
rm -f "${THROWAWAY_GIF}"
}
trap cleanup EXIT INT TERM
# Kill stale ttyd / headless-Chrome processes left over from prior failed
# vhs runs. VHS uses go-rod, which writes its Chrome profile to a
# `rod/user-data` tmpdir; if a prior run died, the locked profile causes
# the next run to fail with `could not open ttyd: ERR_CONNECTION_REFUSED`.
preflight_cleanup_vhs() {
local stale=0
if pgrep -f "ttyd --port" >/dev/null 2>&1; then
pkill -f "ttyd --port" 2>/dev/null || true
stale=1
fi
if pgrep -f "rod/user-data" >/dev/null 2>&1; then
pkill -f "rod/user-data" 2>/dev/null || true
stale=1
fi
if [[ "${stale}" -eq 1 ]]; then
echo "Cleaned up stale ttyd / Chrome processes from prior vhs run."
sleep 1
fi
rm -rf "${TMPDIR:-/tmp}/rod" 2>/dev/null || true
}
preflight_cleanup_vhs
# Dependency checks
for cmd in vhs cargo setcap; do
if ! command -v "$cmd" &> /dev/null; then
echo "Error: $cmd is not installed."
exit 1
fi
done
if ! command -v gifsicle &> /dev/null; then
echo "Note: gifsicle not found; skipping GIF optimization."
HAS_GIFSICLE=0
else
HAS_GIFSICLE=1
fi
# Tape files must exist
[[ -f "${DEMO_TAPE}" ]] || { echo "Error: ${DEMO_TAPE} missing."; exit 1; }
[[ -f "${SCREENSHOTS_TAPE}" ]] || { echo "Error: ${SCREENSHOTS_TAPE} missing."; exit 1; }
# Validate sudo up front (before the potentially long build) so the run
# doesn't die at the setcap step minutes in. Honors SUDO_ASKPASS when
# the caller exports one; otherwise prompts on the terminal as usual.
SUDO=(sudo)
[[ -n "${SUDO_ASKPASS:-}" ]] && SUDO=(sudo -A)
echo "setcap needs sudo; validating credentials..."
"${SUDO[@]}" -v || { echo "Error: sudo authentication failed."; exit 1; }
# Build release binary if missing or stale
if [[ ! -x "${RELEASE_BIN}" ]] || [[ "${RUSTNET_DIR}/Cargo.lock" -nt "${RELEASE_BIN}" ]]; then
echo "Building release binary..."
(cd "${RUSTNET_DIR}" && cargo build --release)
fi
# Grant capture capabilities so the tapes can run rustnet unprivileged.
# File capabilities are dropped whenever cargo rewrites the binary, so
# (re)apply them on every run.
echo "Granting capture capabilities (sudo setcap)..."
"${SUDO[@]}" setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' "${RELEASE_BIN}"
mkdir -p "${SCREENSHOTS_DIR}"
echo ""
echo "Rendering demo GIF (vhs demo.tape)..."
start_traffic
(cd "${RUSTNET_DIR}" && vhs "${DEMO_TAPE}")
stop_traffic
echo ""
echo "Rendering screenshots (vhs screenshots.tape)..."
start_traffic
(cd "${RUSTNET_DIR}" && vhs "${SCREENSHOTS_TAPE}")
stop_traffic
# Optimize the GIF. The lossy pass shrinks dithered/AA edges; flat
# terminal colors are visually unaffected.
if [[ "${HAS_GIFSICLE}" -eq 1 ]] && [[ -f "${GIF_FILE}" ]]; then
echo ""
echo "Optimizing GIF with gifsicle -O3 --lossy=80..."
BEFORE_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}")
gifsicle -O3 --lossy=80 --batch "${GIF_FILE}"
AFTER_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}")
echo " ${BEFORE_SIZE} -> ${AFTER_SIZE} bytes"
fi
# Summary.
echo ""
echo "Done."
echo ""
echo "GIF:"
ls -lh "${GIF_FILE}" 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
echo ""
echo "Screenshots:"
for png in "${SCREENSHOTS_DIR}"/*.png; do
[[ -f "${png}" ]] && ls -lh "${png}" | awk '{print " " $9 " (" $5 ")"}'
done
echo ""
echo "Preview:"
echo " open ${GIF_FILE}"
echo " open ${SCREENSHOTS_DIR}"
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
set -e
UBUNTU_RELEASE=${1:-noble}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
echo "Testing Debian package build for Ubuntu $UBUNTU_RELEASE"
echo "=================================================="
# Build the Docker container
docker build -t rustnet-deb-test:$UBUNTU_RELEASE -f - "$PROJECT_DIR" <<EOF
FROM ubuntu:$UBUNTU_RELEASE
# Install build dependencies
RUN apt-get update && apt-get install -y \\
debhelper \\
devscripts \\
dpkg-dev \\
rustup \\
libpcap-dev \\
libelf-dev \\
elfutils \\
zlib1g-dev \\
clang \\
llvm \\
pkg-config \\
lintian \\
file
WORKDIR /build
COPY . /build/
# Build the source package
RUN echo "Building source package..." && \\
debuild -S -sa -d -us -uc
# Build the binary package (simulates what Launchpad does)
RUN echo "Building binary package..." && \\
cd .. && \\
dpkg-source -x rustnet-monitor_*.dsc extracted && \\
cd extracted && \\
dpkg-buildpackage -b -uc -us
# List the built packages
RUN echo "Built packages:" && \\
ls -lh /build/../*.deb || true
# Run lintian on the package
RUN echo "Running lintian checks..." && \\
lintian /build/../*.deb || true
# Test the package contents
RUN echo "Package contents:" && \\
dpkg-deb -c /build/../rustnet_*.deb
CMD ["/bin/bash"]
EOF
echo ""
echo "Build completed successfully!"
echo ""
echo "To extract the .deb file, run:"
echo " docker create --name rustnet-deb-extract rustnet-deb-test:$UBUNTU_RELEASE"
echo " docker cp rustnet-deb-extract:/build/../rustnet_*.deb ."
echo " docker rm rustnet-deb-extract"