- gNMI integration: NETCONF script to enable gRPC on all 9 routers, Telegraf container with gnmi input plugin, InfluxDB for time-series storage, 3 Grafana telemetry dashboards (utilization, errors, combined) - Traffic generator: Scapy-based dual-mode container (sender/responder) with Flask API, RFC 2544 test suite (throughput, latency, frame-loss, back-to-back), Vue 3 web UI with flow builder, test runner, real-time stats monitor, and results export - docker-compose.yml updated with influxdb, telegraf, traffic-gen, traffic-gen-ui services - Full documentation in DOCS.md sections 15-16 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
121 lines
3.7 KiB
Python
121 lines
3.7 KiB
Python
"""
|
|
Packet Builder - constructs Scapy packets from flow configuration.
|
|
|
|
Each generated packet embeds:
|
|
- Magic bytes b'TGEN' (4 bytes)
|
|
- Sequence number (4 bytes, big-endian)
|
|
- Sender timestamp in nanoseconds (8 bytes, big-endian)
|
|
- Padding to reach requested frame_size
|
|
"""
|
|
|
|
import struct
|
|
import time
|
|
|
|
from scapy.all import (
|
|
Ether, IP, UDP, TCP, ICMP, Dot1Q, Raw, conf,
|
|
)
|
|
|
|
MAGIC = b'TGEN'
|
|
HEADER_LEN = 4 + 4 + 8 # magic + seq + timestamp_ns
|
|
|
|
|
|
def _build_payload(seq: int, frame_size: int, header_overhead: int) -> Raw:
|
|
"""Build payload with magic bytes, sequence number, timestamp placeholder,
|
|
and padding to reach the desired frame_size."""
|
|
timestamp_ns = time.time_ns()
|
|
header = MAGIC + struct.pack('!I', seq) + struct.pack('!Q', timestamp_ns)
|
|
# frame_size includes Ethernet header (14) + FCS (4) in standard accounting,
|
|
# but Scapy doesn't add FCS, so we target frame_size - 4 total bytes on wire.
|
|
# header_overhead accounts for Ether + IP + L4 headers already present.
|
|
pad_len = max(0, frame_size - 4 - header_overhead - HEADER_LEN)
|
|
return Raw(load=header + (b'\x00' * pad_len))
|
|
|
|
|
|
def stamp_payload(payload_bytes: bytes, seq: int) -> bytes:
|
|
"""Re-stamp an existing payload with a new sequence number and fresh timestamp."""
|
|
timestamp_ns = time.time_ns()
|
|
return (
|
|
MAGIC
|
|
+ struct.pack('!I', seq)
|
|
+ struct.pack('!Q', timestamp_ns)
|
|
+ payload_bytes[HEADER_LEN:]
|
|
)
|
|
|
|
|
|
def parse_payload(payload_bytes: bytes):
|
|
"""Extract (seq, timestamp_ns) from a TGEN payload, or None if invalid."""
|
|
if len(payload_bytes) < HEADER_LEN:
|
|
return None
|
|
if payload_bytes[:4] != MAGIC:
|
|
return None
|
|
seq = struct.unpack('!I', payload_bytes[4:8])[0]
|
|
timestamp_ns = struct.unpack('!Q', payload_bytes[8:16])[0]
|
|
return seq, timestamp_ns
|
|
|
|
|
|
def build_packet(flow_config: dict, seq: int = 0):
|
|
"""Build a Scapy packet from a flow configuration dict.
|
|
|
|
Required keys:
|
|
dst_ip, protocol
|
|
|
|
Optional keys:
|
|
src_mac, dst_mac, src_ip, src_port, dst_port, dscp, vlan_id, frame_size
|
|
"""
|
|
protocol = flow_config.get('protocol', 'udp').lower()
|
|
frame_size = flow_config.get('frame_size', 512)
|
|
|
|
# --- Layer 2 ---
|
|
src_mac = flow_config.get('src_mac', 'auto')
|
|
dst_mac = flow_config.get('dst_mac')
|
|
|
|
ether_kwargs = {}
|
|
if src_mac and src_mac != 'auto':
|
|
ether_kwargs['src'] = src_mac
|
|
if dst_mac:
|
|
ether_kwargs['dst'] = dst_mac
|
|
|
|
pkt = Ether(**ether_kwargs)
|
|
header_overhead = 14 # Ethernet
|
|
|
|
# --- VLAN ---
|
|
vlan_id = flow_config.get('vlan_id')
|
|
if vlan_id is not None:
|
|
pkt = pkt / Dot1Q(vlan=int(vlan_id))
|
|
header_overhead += 4
|
|
|
|
# --- Layer 3 ---
|
|
ip_kwargs = {'dst': flow_config['dst_ip']}
|
|
src_ip = flow_config.get('src_ip')
|
|
if src_ip:
|
|
ip_kwargs['src'] = src_ip
|
|
|
|
dscp = flow_config.get('dscp', 0)
|
|
if dscp:
|
|
ip_kwargs['tos'] = int(dscp) << 2
|
|
|
|
pkt = pkt / IP(**ip_kwargs)
|
|
header_overhead += 20 # IP (no options)
|
|
|
|
# --- Layer 4 ---
|
|
if protocol == 'udp':
|
|
src_port = flow_config.get('src_port', 12000)
|
|
dst_port = flow_config.get('dst_port', 5001)
|
|
pkt = pkt / UDP(sport=int(src_port), dport=int(dst_port))
|
|
header_overhead += 8
|
|
elif protocol == 'tcp':
|
|
src_port = flow_config.get('src_port', 12000)
|
|
dst_port = flow_config.get('dst_port', 80)
|
|
pkt = pkt / TCP(sport=int(src_port), dport=int(dst_port), flags='S')
|
|
header_overhead += 20
|
|
elif protocol == 'icmp':
|
|
pkt = pkt / ICMP()
|
|
header_overhead += 8
|
|
else:
|
|
raise ValueError(f'Unsupported protocol: {protocol}')
|
|
|
|
# --- Payload ---
|
|
pkt = pkt / _build_payload(seq, frame_size, header_overhead)
|
|
|
|
return pkt
|