625 lines
24 KiB
Markdown
625 lines
24 KiB
Markdown
# System Architecture
|
||
|
||
This document breaks down how the system is designed and why certain architectural decisions were made.
|
||
|
||
## High Level Architecture
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ CLI Interface (Typer) │
|
||
│ main.py │
|
||
└────────────────────┬─────────────────────────────────────┘
|
||
│
|
||
┌───────────┼───────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌────────┐ ┌────────┐ ┌──────────┐
|
||
│Capture │ │Analyze │ │Visualize │
|
||
│ Engine │ │ PCAP │ │ Charts │
|
||
└───┬────┘ └───┬────┘ └────┬─────┘
|
||
│ │ │
|
||
│ ┌─────┴──────┐ │
|
||
│ │ │ │
|
||
▼ ▼ ▼ ▼
|
||
┌─────────────────────────────────┐
|
||
│ Producer-Consumer Queue │
|
||
│ │
|
||
│ ┌──────────┐ ┌─────────────┐│
|
||
│ │ Producer │──>│ Queue ││
|
||
│ │ (Scapy) │ │ (bounded) ││
|
||
│ └──────────┘ └──────┬──────┘│
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌───────────┐ │
|
||
│ │ Consumer │ │
|
||
│ │ (Process) │ │
|
||
│ └─────┬─────┘ │
|
||
└────────────────────────┼───────┘
|
||
│
|
||
▼
|
||
┌─────────────────┐
|
||
│ Statistics │
|
||
│ Collector │
|
||
│ (Thread-Safe) │
|
||
└────────┬────────┘
|
||
│
|
||
┌───────────────────┼────────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌────────┐ ┌─────────┐ ┌────────┐
|
||
│Console │ │ Export │ │ Charts │
|
||
│ Output │ │JSON/CSV │ │ PNG │
|
||
└────────┘ └─────────┘ └────────┘
|
||
```
|
||
|
||
### Component Breakdown
|
||
|
||
**CLI Interface (main.py)**
|
||
- Purpose: Provides user-facing commands (capture, analyze, export, chart)
|
||
- Responsibilities: Argument parsing, command routing, error display
|
||
- Interfaces: Calls CaptureEngine, analyze_pcap_file, and visualization functions
|
||
|
||
**Capture Engine (capture.py)**
|
||
- Purpose: Real-time packet capture with producer-consumer threading
|
||
- Responsibilities: Raw socket management, privilege checking, graceful shutdown
|
||
- Interfaces: AsyncSniffer from Scapy, Queue for threading, StatisticsCollector for metrics
|
||
|
||
**Analyzer (analyzer.py)**
|
||
- Purpose: Protocol identification and packet field extraction
|
||
- Responsibilities: Layer dissection, protocol classification, data structure conversion
|
||
- Interfaces: Accepts Scapy Packet objects, returns PacketInfo dataclasses
|
||
|
||
**Statistics Collector (statistics.py)**
|
||
- Purpose: Thread-safe aggregation of packet metrics
|
||
- Responsibilities: Counter management, bandwidth sampling, endpoint tracking
|
||
- Interfaces: record_packet() called from consumer thread, get_statistics() for snapshots
|
||
|
||
**Filter Builder (filters.py)**
|
||
- Purpose: Type-safe BPF filter expression construction
|
||
- Responsibilities: Input validation, expression combination, BPF syntax generation
|
||
- Interfaces: Fluent API for chaining filters, build() produces BPF string
|
||
|
||
**Visualization (visualization.py)**
|
||
- Purpose: Generate charts from capture statistics
|
||
- Responsibilities: Matplotlib figure creation, chart styling, file export
|
||
- Interfaces: Accepts CaptureStatistics, produces Figure objects
|
||
|
||
**Export (export.py)**
|
||
- Purpose: Serialize capture data to disk formats
|
||
- Responsibilities: JSON/CSV formatting, data structure conversion
|
||
- Interfaces: Takes CaptureStatistics and PacketInfo lists, writes files
|
||
|
||
**Output (output.py)**
|
||
- Purpose: Rich console formatting for terminal display
|
||
- Responsibilities: Table generation, progress bars, colored output
|
||
- Interfaces: Console singleton, print_* functions for different data types
|
||
|
||
## Data Flow
|
||
|
||
### Live Packet Capture Flow
|
||
|
||
Step by step walkthrough of what happens during live capture:
|
||
|
||
```
|
||
1. User runs command → main.py:capture() (line 110)
|
||
Parses arguments (interface, filter, count, timeout)
|
||
Creates CaptureConfig dataclass
|
||
|
||
2. Config → CaptureEngine.__init__() (line 46)
|
||
Initializes Queue(maxsize=10000)
|
||
Creates StatisticsCollector
|
||
Sets up threading.Event for shutdown coordination
|
||
|
||
3. CaptureEngine.start() → AsyncSniffer.start() (line 112)
|
||
Scapy starts producer thread
|
||
Calls _enqueue_packet callback for each packet
|
||
Producer: packet → Queue.put_nowait()
|
||
|
||
4. Consumer thread _process_packets() runs in parallel (line 72)
|
||
Loop: Queue.get() → extract_packet_info() → record_packet()
|
||
Packet → analyzer.py:extract_packet_info() (line 51)
|
||
PacketInfo → statistics.py:record_packet() (line 47)
|
||
|
||
5. Statistics update (thread-safe with lock) (line 48-67)
|
||
Increment counters (total_packets, total_bytes)
|
||
Update protocol_distribution dict
|
||
Update endpoint statistics
|
||
Check if bandwidth sample interval elapsed
|
||
|
||
6. User presses Ctrl+C → GracefulCapture handles signal
|
||
Sets stop_event → consumer thread exits
|
||
Calls sniffer.stop() → producer thread exits
|
||
Returns final CaptureStatistics snapshot
|
||
|
||
7. Statistics → output.py:print_*() functions (line 84-170)
|
||
Formats Rich tables for protocols, top talkers
|
||
Displays bandwidth graphs
|
||
Shows capture summary panel
|
||
```
|
||
|
||
Example with code references:
|
||
|
||
```python
|
||
# Entry point: main.py:159
|
||
def capture(interface, filter_expr, count, timeout, output, verbose):
|
||
config = CaptureConfig(
|
||
interface = interface,
|
||
bpf_filter = filter_expr,
|
||
packet_count = count,
|
||
timeout_seconds = timeout,
|
||
)
|
||
|
||
# Producer-consumer setup: capture.py:112-131
|
||
engine = CaptureEngine(config=config)
|
||
engine.start() # Spawns threads
|
||
|
||
# Processing loop: capture.py:72-90
|
||
while not self._stop_event.is_set():
|
||
packet = self._queue.get()
|
||
info = extract_packet_info(packet) # analyzer.py:51
|
||
self._stats.record_packet(info) # statistics.py:47
|
||
```
|
||
|
||
### PCAP File Analysis Flow
|
||
|
||
```
|
||
1. User: netanal analyze traffic.pcap
|
||
↓
|
||
2. main.py:analyze() (line 237)
|
||
Validates file exists
|
||
↓
|
||
3. analyzer.py:analyze_pcap_file() (line 162)
|
||
Opens PcapReader (memory efficient iteration)
|
||
↓
|
||
4. For each packet in file:
|
||
extract_packet_info() → PacketInfo
|
||
StatisticsCollector.record_packet()
|
||
↓
|
||
5. Returns CaptureStatistics
|
||
↓
|
||
6. output.py formats and displays
|
||
Protocol table, top talkers, summary
|
||
```
|
||
|
||
## Design Patterns
|
||
|
||
### Producer-Consumer Pattern
|
||
|
||
**What it is:**
|
||
Separates data generation from data processing using a queue buffer. Producer threads add items to queue, consumer threads remove and process items. Decouples rate of production from rate of consumption.
|
||
|
||
**Where we use it:**
|
||
`capture.py:46-90` implements the full pattern. AsyncSniffer is the producer, _process_packets loop is the consumer.
|
||
|
||
**Why we chose it:**
|
||
Packet capture must run at wire speed without dropping packets. Processing (protocol identification, statistics updates, optional callbacks) is slower. Buffering in a queue prevents packet loss when processing lags.
|
||
|
||
**Trade-offs:**
|
||
- Pros: Prevents packet loss, decouples concerns, enables parallelism
|
||
- Cons: Uses memory for queue buffer, adds latency (packets delayed in queue), requires thread synchronization
|
||
|
||
Example implementation:
|
||
```python
|
||
# capture.py:64-70 - Producer callback
|
||
def _enqueue_packet(self, packet: Packet) -> None:
|
||
try:
|
||
self._queue.put_nowait(packet)
|
||
except Full:
|
||
with self._count_lock:
|
||
self._dropped_packets += 1
|
||
|
||
# capture.py:72-90 - Consumer loop
|
||
def _process_packets(self) -> None:
|
||
while not self._stop_event.is_set():
|
||
try:
|
||
packet = self._queue.get(timeout=0.1)
|
||
except Empty:
|
||
continue
|
||
|
||
info = extract_packet_info(packet)
|
||
self._stats.record_packet(info)
|
||
```
|
||
|
||
The producer never blocks on slow processing. The consumer processes at its own pace. If queue fills, packets drop with counter increment rather than crashing.
|
||
|
||
### Builder Pattern
|
||
|
||
**What it is:**
|
||
Constructs complex objects step by step through a fluent interface. Each method returns self, enabling method chaining. Final build() call produces the result.
|
||
|
||
**Where we use it:**
|
||
`filters.py:48-175` implements FilterBuilder for BPF expressions.
|
||
|
||
**Why we chose it:**
|
||
BPF syntax is error-prone. Users can build type-safe filters with validation at each step rather than error-prone string concatenation.
|
||
|
||
**Trade-offs:**
|
||
- Pros: Type safety, input validation, readable API, prevents injection
|
||
- Cons: More code than raw strings, requires understanding the builder
|
||
|
||
Example:
|
||
```python
|
||
# filters.py:48-175
|
||
filter_expr = (
|
||
FilterBuilder()
|
||
.protocol(Protocol.TCP)
|
||
.port(443)
|
||
.host("192.168.1.1")
|
||
.build()
|
||
)
|
||
# Result: "(tcp) and port 443 and host 192.168.1.1"
|
||
|
||
# Validates each input:
|
||
# filters.py:30-37
|
||
def _validate_port(port_number: int) -> None:
|
||
if not PortRange.MIN <= port_number <= PortRange.MAX:
|
||
raise ValidationError(f"Port must be 0-65535, got {port_number}")
|
||
```
|
||
|
||
### Dataclass with Slots Pattern
|
||
|
||
**What it is:**
|
||
Python dataclasses with `slots=True` reduce memory usage by storing attributes in fixed slots instead of a dict. Frozen dataclasses are immutable.
|
||
|
||
**Where we use it:**
|
||
All models in `models.py:11-159` use dataclasses with slots.
|
||
|
||
**Why we chose it:**
|
||
Packet captures generate thousands to millions of PacketInfo objects. Slots reduce per-object memory by ~40%. Immutability prevents accidental modification.
|
||
|
||
**Trade-offs:**
|
||
- Pros: Lower memory usage, immutability safety, clear schema
|
||
- Cons: Cannot add attributes dynamically, slightly slower instantiation
|
||
|
||
Example:
|
||
```python
|
||
# models.py:22-35
|
||
@dataclass(frozen = True, slots = True)
|
||
class PacketInfo:
|
||
timestamp: float
|
||
src_ip: str
|
||
dst_ip: str
|
||
protocol: Protocol
|
||
size: int
|
||
src_port: int | None = None
|
||
dst_port: int | None = None
|
||
src_mac: str | None = None
|
||
dst_mac: str | None = None
|
||
```
|
||
|
||
With 1 million packets, slots save ~40MB compared to dict-based attributes.
|
||
|
||
### Context Manager Pattern
|
||
|
||
**What it is:**
|
||
Objects implementing `__enter__` and `__exit__` for resource setup and cleanup. Used with `with` statements to ensure cleanup even on exceptions.
|
||
|
||
**Where we use it:**
|
||
`capture.py:197-230` implements GracefulCapture context manager.
|
||
|
||
**Why we chose it:**
|
||
Ensures graceful shutdown even if user Ctrl+C's or exceptions occur. Signal handlers restore properly and capture stops cleanly.
|
||
|
||
**Trade-offs:**
|
||
- Pros: Guaranteed cleanup, clean syntax, exception safe
|
||
- Cons: Additional boilerplate, understanding `__enter__/__exit__` protocol
|
||
|
||
Example:
|
||
```python
|
||
# capture.py:197-230
|
||
class GracefulCapture:
|
||
def __enter__(self) -> CaptureEngine:
|
||
# Setup: Install signal handlers
|
||
self._original_sigint = signal.signal(signal.SIGINT, self._handle_signal)
|
||
self._engine.start()
|
||
return self._engine
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
# Cleanup: Restore handlers, stop capture
|
||
signal.signal(signal.SIGINT, self._original_sigint)
|
||
self._engine.stop()
|
||
|
||
# Usage: main.py:159-167
|
||
with GracefulCapture(engine) as cap:
|
||
stats = cap.wait()
|
||
```
|
||
|
||
## Layer Separation
|
||
|
||
```
|
||
┌────────────────────────────────────┐
|
||
│ CLI Layer (main.py) │
|
||
│ - Command definitions │
|
||
│ - Argument parsing │
|
||
│ - User interaction │
|
||
└────────────────────────────────────┘
|
||
↓ calls
|
||
┌────────────────────────────────────┐
|
||
│ Service Layer │
|
||
│ - capture.py: CaptureEngine │
|
||
│ - analyzer.py: Protocol logic │
|
||
│ - filters.py: Filter building │
|
||
└────────────────────────────────────┘
|
||
↓ uses
|
||
┌────────────────────────────────────┐
|
||
│ Data Layer │
|
||
│ - statistics.py: Aggregation │
|
||
│ - models.py: Data structures │
|
||
│ - constants.py: Configuration │
|
||
└────────────────────────────────────┘
|
||
↓ produces
|
||
┌────────────────────────────────────┐
|
||
│ Output Layer │
|
||
│ - output.py: Console display │
|
||
│ - visualization.py: Charts │
|
||
│ - export.py: File I/O │
|
||
└────────────────────────────────────┘
|
||
```
|
||
|
||
### Why Layers?
|
||
|
||
Separation of concerns prevents tight coupling. CLI commands don't know about Scapy internals. CaptureEngine doesn't know about Rich formatting. Changes to visualization don't affect statistics collection.
|
||
|
||
### What Lives Where
|
||
|
||
**CLI Layer (main.py):**
|
||
- Files: main.py, __main__.py
|
||
- Imports: Can import from all layers
|
||
- Forbidden: Direct Scapy usage, Rich formatting (delegate to output.py), Matplotlib (delegate to visualization.py)
|
||
|
||
**Service Layer:**
|
||
- Files: capture.py, analyzer.py, filters.py
|
||
- Imports: Data layer only, no CLI or output dependencies
|
||
- Forbidden: print statements (return data instead), sys.exit() (raise exceptions)
|
||
|
||
**Data Layer:**
|
||
- Files: statistics.py, models.py, constants.py, exceptions.py
|
||
- Imports: Only standard library and type hints
|
||
- Forbidden: Any I/O, any third-party imports (except type checking)
|
||
|
||
**Output Layer:**
|
||
- Files: output.py, visualization.py, export.py
|
||
- Imports: Data layer for models, third-party formatting libraries
|
||
- Forbidden: Business logic, packet processing
|
||
|
||
## Data Models
|
||
|
||
### PacketInfo
|
||
|
||
```python
|
||
# models.py:22-35
|
||
@dataclass(frozen = True, slots = True)
|
||
class PacketInfo:
|
||
timestamp: float
|
||
src_ip: str
|
||
dst_ip: str
|
||
protocol: Protocol
|
||
size: int
|
||
src_port: int | None = None
|
||
dst_port: int | None = None
|
||
src_mac: str | None = None
|
||
dst_mac: str | None = None
|
||
```
|
||
|
||
**Fields explained:**
|
||
- `timestamp`: Unix epoch time from packet capture. Float for microsecond precision. Used for bandwidth calculations and time-series analysis.
|
||
- `src_ip/dst_ip`: String IP addresses (IPv4 or IPv6). Not validated at model level (analyzer validates). Used for endpoint tracking.
|
||
- `protocol`: Protocol enum (TCP, UDP, ICMP, etc). Determined by analyzer.identify_protocol(). Used for distribution statistics.
|
||
- `size`: Total packet size in bytes including all headers. Used for bandwidth and traffic volume calculations.
|
||
- `src_port/dst_port`: Optional because ICMP/ARP don't have ports. None means not applicable or not extracted.
|
||
- `src_mac/dst_mac`: Optional Layer 2 addresses. Useful for local network analysis, less relevant for routed traffic.
|
||
|
||
**Relationships:**
|
||
- Frozen dataclass prevents accidental modification after creation
|
||
- Created by analyzer.extract_packet_info() from Scapy Packet objects
|
||
- Consumed by statistics.StatisticsCollector.record_packet()
|
||
- Stored in lists for export but not kept in memory during live capture (only statistics)
|
||
|
||
### EndpointStats
|
||
|
||
```python
|
||
# models.py:38-61
|
||
@dataclass(slots = True)
|
||
class EndpointStats:
|
||
ip_address: str
|
||
packets_sent: int = 0
|
||
packets_received: int = 0
|
||
bytes_sent: int = 0
|
||
bytes_received: int = 0
|
||
|
||
@property
|
||
def total_packets(self) -> int:
|
||
return self.packets_sent + self.packets_received
|
||
|
||
@property
|
||
def total_bytes(self) -> int:
|
||
return self.bytes_sent + self.bytes_received
|
||
```
|
||
|
||
**Purpose:** Track bidirectional traffic for each IP address. Used for "top talkers" identification and baseline establishment.
|
||
|
||
**Relationships:**
|
||
- Mutable (not frozen) because counters increment throughout capture
|
||
- One instance per unique IP address seen
|
||
- Stored in statistics.StatisticsCollector._endpoints dict
|
||
- Properties enable sorting by total volume without storing redundant fields
|
||
|
||
## Security Architecture
|
||
|
||
### Threat Model
|
||
|
||
What we're protecting against:
|
||
|
||
1. **Privilege escalation** - Ensure packet capture only works with proper permissions. No bypassing OS security. Check capabilities explicitly before attempting capture.
|
||
|
||
2. **Filter injection** - Malicious filter strings could crash the kernel or bypass intended restrictions. Validate all user input before passing to BPF compiler.
|
||
|
||
3. **Resource exhaustion** - Unbounded queues or memory usage could DoS the monitoring system. Use bounded buffers and reasonable limits.
|
||
|
||
What we're NOT protecting against (out of scope):
|
||
|
||
- **Physical network access** - Assume attacker can plug into the network. This tool doesn't prevent that.
|
||
- **Encrypted payload inspection** - We analyze metadata and headers, not encrypted content. TLS decryption requires MITM proxies.
|
||
- **Quantum computing threats** - Future attacks on cryptographic protocols aren't addressed by packet capture tools.
|
||
|
||
### Defense Layers
|
||
|
||
```
|
||
Layer 1: Privilege Validation
|
||
↓ (capture.py:341-375)
|
||
Layer 2: Input Validation
|
||
↓ (filters.py:30-66, main.py)
|
||
Layer 3: Resource Limits
|
||
↓ (capture.py:46, constants.py)
|
||
Layer 4: Error Handling
|
||
↓ (exceptions.py, try/except throughout)
|
||
```
|
||
|
||
**Why multiple layers?**
|
||
|
||
Defense in depth. If input validation has a bug, resource limits prevent DoS. If privilege check bypasses, kernel still enforces permissions. Each layer catches different attack vectors.
|
||
|
||
## Storage Strategy
|
||
|
||
### In-Memory Statistics
|
||
|
||
**What we store:**
|
||
- Aggregate counters (total packets, bytes)
|
||
- Per-protocol distributions
|
||
- Per-endpoint statistics
|
||
- Bandwidth samples (time-series)
|
||
|
||
**Why in-memory:**
|
||
Performance. Disk I/O during high-speed capture drops packets. Statistics update on every packet, requiring nanosecond latency. RAM provides this, disk does not.
|
||
|
||
**Memory management:**
|
||
```python
|
||
# constants.py:36-41
|
||
class CaptureDefaults:
|
||
QUEUE_SIZE: Final[int] = 10_000
|
||
BANDWIDTH_SAMPLE_INTERVAL_SECONDS: Final[float] = 1.0
|
||
```
|
||
|
||
Queue size limits memory to ~10K packets × ~1.5KB = 15MB max. Bandwidth samples at 1/second means 3600 samples/hour = ~100KB/hour. Endpoint stats depend on unique IPs seen.
|
||
|
||
### Disk Export
|
||
|
||
Optional export to JSON/CSV for persistence:
|
||
```python
|
||
# export.py:80-107
|
||
def export_to_json(
|
||
stats: CaptureStatistics,
|
||
filepath: Path,
|
||
packets: list[PacketInfo] | None = None,
|
||
options: ExportOptions | None = None,
|
||
) -> None:
|
||
```
|
||
|
||
Only happens on demand, not during capture. Separates hot path (capture) from cold path (analysis).
|
||
|
||
## Configuration
|
||
|
||
### Environment Variables
|
||
|
||
```bash
|
||
NO_COLOR=1 # Disables colored output for CI/CD environments
|
||
CI=1 # Optimizes output for continuous integration
|
||
PYTHONUNBUFFERED=1 # Forces unbuffered stdout for real-time logs
|
||
```
|
||
|
||
### Configuration Strategy
|
||
|
||
Constants in `constants.py` provide sensible defaults. Command-line arguments override defaults. No config files to avoid complexity for a simple tool.
|
||
|
||
**Development:**
|
||
```python
|
||
# constants.py provides overridable defaults
|
||
CaptureDefaults.QUEUE_SIZE = 10_000 # Balance memory vs packet loss
|
||
```
|
||
|
||
**Production:**
|
||
Adjust queue size based on available memory and expected packet rate. 10K queue handles ~1-2 Gbps sustained traffic.
|
||
|
||
## Performance Considerations
|
||
|
||
### Bottlenecks
|
||
|
||
Where this system gets slow under load:
|
||
|
||
1. **Queue contention** - Producer and consumer both access queue. At extreme rates (10+ Gbps), queue operations become serialization point. Mitigate with multiple queues and worker threads.
|
||
|
||
2. **Statistics lock** - Every packet acquisition requires lock in record_packet(). At millions of packets/second, lock contention dominates. Mitigate with lock-free counters or per-thread statistics with periodic merging.
|
||
|
||
### Optimizations
|
||
|
||
What we did to make it faster:
|
||
|
||
- **BPF filtering in kernel**: Drops ~99% of irrelevant packets before userspace sees them. Moving from userspace to BPF filter reduced CPU usage from 80% to 5% in testing with port 80 filter on busy network.
|
||
|
||
- **Bounded queue with non-blocking put**: Using `put_nowait()` with explicit dropped counter prevents producer blocking. Capture thread never waits on slow consumer.
|
||
|
||
- **Dataclass slots**: Reduces memory per packet by 40%. With 10K queue, saves 6MB. Allows larger queues in same memory budget.
|
||
|
||
- **Minimal string formatting**: Only format output when displaying, not during capture. `print_packet()` only called if `--verbose` flag set.
|
||
|
||
### Scalability
|
||
|
||
**Vertical scaling:**
|
||
Add more CPU/RAM to single machine. Packet capture is CPU-bound (protocol parsing) and memory-bound (queue storage). 8-core system with 32GB RAM can handle ~5-10 Gbps depending on traffic mix.
|
||
|
||
**Horizontal scaling:**
|
||
Requires architectural changes:
|
||
- Mirror traffic to multiple capture hosts
|
||
- Use distributed queue (Kafka/RabbitMQ) instead of in-memory Queue
|
||
- Aggregate statistics from multiple collectors
|
||
- Current code doesn't support this without modification
|
||
|
||
## Design Decisions
|
||
|
||
### Decision 1: AsyncSniffer vs sync sniff()
|
||
|
||
**What we chose:**
|
||
AsyncSniffer with background thread
|
||
|
||
**Alternatives considered:**
|
||
- `sniff(prn=callback)` - Rejected because blocks the main thread, preventing graceful shutdown and progress display
|
||
- `sniff(timeout=1)` in loop - Rejected because introduces gaps where packets can be lost between timeout and restart
|
||
|
||
**Trade-offs:**
|
||
Gained: Responsive UI, graceful shutdown, concurrent processing
|
||
Lost: Slightly more complex threading logic, need for queue management
|
||
|
||
### Decision 2: Thread locks vs lock-free algorithms
|
||
|
||
**What we chose:**
|
||
`threading.Lock()` for statistics protection
|
||
|
||
**Alternatives considered:**
|
||
- Lock-free atomics - Rejected because Python doesn't have true atomic operations (GIL exists but doesn't help here)
|
||
- No synchronization - Rejected because causes race conditions and data corruption
|
||
|
||
**Trade-offs:**
|
||
Gained: Correctness, simplicity, standard patterns
|
||
Lost: Some performance at extreme packet rates (millions/sec), potential for lock contention
|
||
|
||
### Decision 3: Dataclasses vs named tuples
|
||
|
||
**What we chose:**
|
||
Frozen dataclasses with slots
|
||
|
||
**Alternatives considered:**
|
||
- Named tuples - Rejected because lack type checking, no default values, harder to extend
|
||
- Regular classes - Rejected because boilerplate code, no automatic `__repr__`, more memory
|
||
|
||
**Trade-offs:**
|
||
Gained: Type safety, defaults, less boilerplate, better memory usage
|
||
Lost: Requires Python 3.10+ for slots in dataclasses
|
||
|
||
## Next Steps
|
||
|
||
Now that you understand the architecture:
|
||
|
||
1. Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) for code walkthrough showing how each component actually works
|
||
2. Try modifying queue size in constants.py and observe impact on packet loss under load
|
||
3. Trace a single packet from capture through statistics to output by adding debug prints
|