add learn folder, move hidden files, update root readme, add learn folder, add clang tidy, and format
This commit is contained in:
parent
6f26091240
commit
9397b12a4a
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
path: PROJECTS/beginner/metadata-scrubber-tool
|
||||
- name: network-traffic-analyzer
|
||||
type: ruff
|
||||
path: PROJECTS/beginner/network-traffic-analyzer
|
||||
path: PROJECTS/beginner/network-traffic-analyzer/python
|
||||
- name: base64-tool
|
||||
type: ruff
|
||||
path: PROJECTS/beginner/base64-tool
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ on:
|
|||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'PROJECTS/beginner/network-traffic-analyzer/**'
|
||||
- '!PROJECTS/beginner/network-traffic-analyzer/README.md'
|
||||
- '!PROJECTS/beginner/network-traffic-analyzer/justfile'
|
||||
- 'PROJECTS/beginner/network-traffic-analyzer/python/**'
|
||||
- '!PROJECTS/beginner/network-traffic-analyzer/python/README.md'
|
||||
- '!PROJECTS/beginner/network-traffic-analyzer/python/justfile'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
|
||||
- name: Check if version exists on PyPI
|
||||
id: version_check
|
||||
working-directory: PROJECTS/beginner/network-traffic-analyzer
|
||||
working-directory: PROJECTS/beginner/network-traffic-analyzer/python
|
||||
run: |
|
||||
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/netanal/${VERSION}/json")
|
||||
|
|
@ -57,11 +57,11 @@ jobs:
|
|||
|
||||
- name: Build package
|
||||
if: steps.version_check.outputs.exists != 'true'
|
||||
working-directory: PROJECTS/beginner/network-traffic-analyzer
|
||||
working-directory: PROJECTS/beginner/network-traffic-analyzer/python
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: steps.version_check.outputs.exists != 'true'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: PROJECTS/beginner/network-traffic-analyzer/dist/
|
||||
packages-dir: PROJECTS/beginner/network-traffic-analyzer/python/dist/
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ repos:
|
|||
- id: ruff
|
||||
name: ruff check (network-traffic-analyzer)
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
files: ^PROJECTS/beginner/network-traffic-analyzer/
|
||||
files: ^PROJECTS/beginner/network-traffic-analyzer/python/
|
||||
exclude: (\.venv|__pycache__|\.pytest_cache)/
|
||||
|
||||
- id: ruff
|
||||
|
|
|
|||
|
|
@ -41,5 +41,13 @@ class Settings(BaseSettings):
|
|||
batch_size: int = 32
|
||||
batch_timeout_ms: int = 50
|
||||
|
||||
model_dir: str = "data/models"
|
||||
detection_mode: str = "rules"
|
||||
ensemble_weight_ae: float = 0.40
|
||||
ensemble_weight_rf: float = 0.40
|
||||
ensemble_weight_if: float = 0.20
|
||||
ae_threshold_percentile: float = 99.5
|
||||
mlflow_tracking_uri: str = "file:./mlruns"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
autoencoder.py
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import Tensor, nn
|
||||
|
||||
|
||||
class ThreatAutoencoder(nn.Module):
|
||||
"""
|
||||
Symmetric autoencoder for HTTP request anomaly detection.
|
||||
|
||||
Architecture (35-dim input):
|
||||
Encoder: 35 → 24 → 12 → 6 (bottleneck)
|
||||
Decoder: 6 → 12 → 24 → 35
|
||||
|
||||
Trained on normal traffic only. High reconstruction error
|
||||
indicates an anomalous (potentially malicious) request.
|
||||
"""
|
||||
|
||||
def __init__(self, input_dim: int = 35) -> None:
|
||||
super().__init__()
|
||||
self.input_dim = input_dim
|
||||
|
||||
self.encoder = nn.Sequential(
|
||||
nn.Linear(input_dim, 24),
|
||||
nn.BatchNorm1d(24),
|
||||
nn.LeakyReLU(0.2),
|
||||
nn.Dropout(0.2),
|
||||
nn.Linear(24, 12),
|
||||
nn.BatchNorm1d(12),
|
||||
nn.LeakyReLU(0.2),
|
||||
nn.Dropout(0.2),
|
||||
nn.Linear(12, 6),
|
||||
)
|
||||
|
||||
self.decoder = nn.Sequential(
|
||||
nn.Linear(6, 12),
|
||||
nn.BatchNorm1d(12),
|
||||
nn.LeakyReLU(0.2),
|
||||
nn.Dropout(0.2),
|
||||
nn.Linear(12, 24),
|
||||
nn.BatchNorm1d(24),
|
||||
nn.LeakyReLU(0.2),
|
||||
nn.Dropout(0.2),
|
||||
nn.Linear(24, input_dim),
|
||||
nn.Sigmoid(),
|
||||
)
|
||||
|
||||
def encode(self, x: Tensor) -> Tensor:
|
||||
"""
|
||||
Compress input through the encoder to the 6-dim bottleneck.
|
||||
"""
|
||||
return self.encoder(x)
|
||||
|
||||
def decode(self, z: Tensor) -> Tensor:
|
||||
"""
|
||||
Reconstruct input from the bottleneck representation.
|
||||
"""
|
||||
return self.decoder(z)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""
|
||||
Full forward pass: encode then decode.
|
||||
"""
|
||||
return self.decode(self.encode(x))
|
||||
|
||||
def compute_reconstruction_error(self, x: Tensor) -> Tensor:
|
||||
"""
|
||||
Per-sample mean squared error between input and reconstruction.
|
||||
"""
|
||||
reconstructed = self.forward(x)
|
||||
return torch.mean((x - reconstructed) ** 2, dim=1)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
scaler.py
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from sklearn.preprocessing import RobustScaler
|
||||
|
||||
|
||||
class FeatureScaler:
|
||||
"""
|
||||
IQR-based feature scaler persisted as JSON (not pickle).
|
||||
|
||||
Wraps sklearn RobustScaler for outlier-robust normalization.
|
||||
Used only for autoencoder input — tree models are scale-invariant.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._scaler: RobustScaler | None = None
|
||||
self._fitted = False
|
||||
|
||||
@property
|
||||
def n_features(self) -> int:
|
||||
"""
|
||||
Number of features the scaler was fitted on.
|
||||
"""
|
||||
if not self._fitted or self._scaler is None:
|
||||
raise RuntimeError("Scaler has not been fitted")
|
||||
return int(self._scaler.n_features_in_)
|
||||
|
||||
def fit(self, X: np.ndarray) -> FeatureScaler:
|
||||
"""
|
||||
Fit the scaler on training data.
|
||||
"""
|
||||
self._scaler = RobustScaler()
|
||||
self._scaler.fit(X)
|
||||
self._fitted = True
|
||||
return self
|
||||
|
||||
def transform(self, X: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Transform features using the fitted scaler parameters.
|
||||
"""
|
||||
if not self._fitted or self._scaler is None:
|
||||
raise RuntimeError("Scaler has not been fitted")
|
||||
return self._scaler.transform(X).astype(np.float32)
|
||||
|
||||
def inverse_transform(self, X: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Reverse the scaling transformation.
|
||||
"""
|
||||
if not self._fitted or self._scaler is None:
|
||||
raise RuntimeError("Scaler has not been fitted")
|
||||
return self._scaler.inverse_transform(X).astype(np.float32)
|
||||
|
||||
def fit_transform(self, X: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Fit and transform in one step.
|
||||
"""
|
||||
self.fit(X)
|
||||
return self.transform(X)
|
||||
|
||||
def save_json(self, path: Path | str) -> None:
|
||||
"""
|
||||
Serialize scaler parameters to a human-readable JSON file.
|
||||
"""
|
||||
if not self._fitted or self._scaler is None:
|
||||
raise RuntimeError("Scaler has not been fitted")
|
||||
data = {
|
||||
"center": self._scaler.center_.tolist(),
|
||||
"scale": self._scaler.scale_.tolist(),
|
||||
"n_features": int(self._scaler.n_features_in_),
|
||||
}
|
||||
Path(path).write_text(json.dumps(data, indent=2))
|
||||
|
||||
@classmethod
|
||||
def load_json(cls, path: Path | str) -> FeatureScaler:
|
||||
"""
|
||||
Reconstruct a fitted scaler from a JSON file.
|
||||
"""
|
||||
data = json.loads(Path(path).read_text())
|
||||
scaler = cls()
|
||||
scaler._scaler = RobustScaler()
|
||||
scaler._scaler.center_ = np.array(data["center"], dtype=np.float64)
|
||||
scaler._scaler.scale_ = np.array(data["scale"], dtype=np.float64)
|
||||
scaler._scaler.n_features_in_ = data["n_features"]
|
||||
scaler._fitted = True
|
||||
return scaler
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
test_autoencoder.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from ml.autoencoder import ThreatAutoencoder
|
||||
|
||||
|
||||
class TestAutoencoderArchitecture:
|
||||
|
||||
def test_output_shape_matches_input(self) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
x = torch.randn(16, 35)
|
||||
out = model(x)
|
||||
assert out.shape == (16, 35)
|
||||
|
||||
def test_bottleneck_dim_is_six(self) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
x = torch.randn(4, 35)
|
||||
encoded = model.encode(x)
|
||||
assert encoded.shape == (4, 6)
|
||||
|
||||
def test_single_sample_forward(self) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
model.eval()
|
||||
x = torch.randn(1, 35)
|
||||
with torch.no_grad():
|
||||
out = model(x)
|
||||
assert out.shape == (1, 35)
|
||||
|
||||
def test_output_values_in_zero_one_range(self) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
model.eval()
|
||||
x = torch.randn(32, 35)
|
||||
with torch.no_grad():
|
||||
out = model(x)
|
||||
assert out.min() >= 0.0
|
||||
assert out.max() <= 1.0
|
||||
|
||||
def test_reconstruction_error_shape(self) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
model.eval()
|
||||
x = torch.randn(8, 35)
|
||||
with torch.no_grad():
|
||||
errors = model.compute_reconstruction_error(x)
|
||||
assert errors.shape == (8,)
|
||||
|
||||
def test_reconstruction_error_positive(self) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
model.eval()
|
||||
x = torch.randn(8, 35)
|
||||
with torch.no_grad():
|
||||
errors = model.compute_reconstruction_error(x)
|
||||
assert (errors >= 0.0).all()
|
||||
|
||||
def test_trained_model_reconstructs_normal_better_than_anomaly(self) -> None:
|
||||
torch.manual_seed(42)
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
|
||||
|
||||
normal_data = torch.randn(500, 35) * 0.5 + 0.5
|
||||
normal_data = normal_data.clamp(0, 1)
|
||||
|
||||
model.train()
|
||||
for _ in range(50):
|
||||
out = model(normal_data)
|
||||
loss = torch.nn.functional.mse_loss(out, normal_data)
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
normal_errors = model.compute_reconstruction_error(normal_data[:50])
|
||||
anomaly_data = torch.rand(50, 35) * 3.0 - 1.0
|
||||
anomaly_errors = model.compute_reconstruction_error(anomaly_data)
|
||||
|
||||
assert anomaly_errors.mean() > normal_errors.mean()
|
||||
|
||||
def test_eval_mode_disables_dropout(self) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
model.eval()
|
||||
x = torch.randn(4, 35)
|
||||
with torch.no_grad():
|
||||
out1 = model(x)
|
||||
out2 = model(x)
|
||||
assert torch.allclose(out1, out2)
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 8, 32, 128])
|
||||
def test_variable_batch_sizes(self, batch_size: int) -> None:
|
||||
model = ThreatAutoencoder(input_dim=35)
|
||||
model.eval()
|
||||
x = torch.randn(batch_size, 35)
|
||||
with torch.no_grad():
|
||||
out = model(x)
|
||||
assert out.shape == (batch_size, 35)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
test_config_ml.py
|
||||
"""
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
def test_default_detection_mode_is_rules() -> None:
|
||||
assert settings.detection_mode == "rules"
|
||||
|
||||
|
||||
def test_default_ensemble_weights_sum_to_one() -> None:
|
||||
total = (
|
||||
settings.ensemble_weight_ae
|
||||
+ settings.ensemble_weight_rf
|
||||
+ settings.ensemble_weight_if
|
||||
)
|
||||
assert abs(total - 1.0) < 1e-6
|
||||
|
||||
|
||||
def test_default_model_dir() -> None:
|
||||
assert settings.model_dir == "data/models"
|
||||
|
||||
|
||||
def test_default_ae_threshold_percentile() -> None:
|
||||
assert settings.ae_threshold_percentile == 99.5
|
||||
|
||||
|
||||
def test_default_mlflow_tracking_uri() -> None:
|
||||
assert settings.mlflow_tracking_uri == "file:./mlruns"
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
test_scaler.py
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from ml.scaler import FeatureScaler
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_data() -> np.ndarray:
|
||||
rng = np.random.default_rng(42)
|
||||
return rng.standard_normal((200, 35)).astype(np.float32)
|
||||
|
||||
|
||||
class TestFeatureScaler:
|
||||
|
||||
def test_fit_sets_n_features(self, sample_data: np.ndarray) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
assert scaler.n_features == 35
|
||||
|
||||
def test_transform_preserves_shape(self, sample_data: np.ndarray) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
transformed = scaler.transform(sample_data)
|
||||
assert transformed.shape == sample_data.shape
|
||||
|
||||
def test_transform_dtype_float32(self, sample_data: np.ndarray) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
transformed = scaler.transform(sample_data)
|
||||
assert transformed.dtype == np.float32
|
||||
|
||||
def test_transformed_median_near_zero(self, sample_data: np.ndarray) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
transformed = scaler.transform(sample_data)
|
||||
medians = np.median(transformed, axis=0)
|
||||
assert np.allclose(medians, 0.0, atol=0.15)
|
||||
|
||||
def test_inverse_transform_recovers_original(
|
||||
self, sample_data: np.ndarray
|
||||
) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
transformed = scaler.transform(sample_data)
|
||||
recovered = scaler.inverse_transform(transformed)
|
||||
np.testing.assert_allclose(recovered, sample_data, atol=1e-5)
|
||||
|
||||
def test_save_json_creates_file(
|
||||
self, sample_data: np.ndarray, tmp_path: Path
|
||||
) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
path = tmp_path / "scaler.json"
|
||||
scaler.save_json(path)
|
||||
assert path.exists()
|
||||
assert path.stat().st_size > 0
|
||||
|
||||
def test_save_json_is_valid_json(
|
||||
self, sample_data: np.ndarray, tmp_path: Path
|
||||
) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
path = tmp_path / "scaler.json"
|
||||
scaler.save_json(path)
|
||||
data = json.loads(path.read_text())
|
||||
assert "center" in data
|
||||
assert "scale" in data
|
||||
assert "n_features" in data
|
||||
|
||||
def test_load_json_round_trip(
|
||||
self, sample_data: np.ndarray, tmp_path: Path
|
||||
) -> None:
|
||||
scaler = FeatureScaler()
|
||||
scaler.fit(sample_data)
|
||||
path = tmp_path / "scaler.json"
|
||||
scaler.save_json(path)
|
||||
|
||||
loaded = FeatureScaler.load_json(path)
|
||||
assert loaded.n_features == scaler.n_features
|
||||
|
||||
original_out = scaler.transform(sample_data)
|
||||
loaded_out = loaded.transform(sample_data)
|
||||
np.testing.assert_allclose(original_out, loaded_out, atol=1e-6)
|
||||
|
||||
def test_transform_before_fit_raises(self) -> None:
|
||||
scaler = FeatureScaler()
|
||||
with pytest.raises(RuntimeError):
|
||||
scaler.transform(np.zeros((5, 35), dtype=np.float32))
|
||||
|
||||
def test_fit_transform_convenience(self, sample_data: np.ndarray) -> None:
|
||||
scaler = FeatureScaler()
|
||||
result = scaler.fit_transform(sample_data)
|
||||
assert result.shape == sample_data.shape
|
||||
assert scaler.n_features == 35
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
"""
|
||||
©AngelaMos | 2026
|
||||
test_training.py
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from ml.train_autoencoder import train_autoencoder
|
||||
from ml.train_classifiers import train_isolation_forest, train_random_forest
|
||||
|
||||
|
||||
class TestAutoencoderTraining:
|
||||
|
||||
@pytest.fixture
|
||||
def normal_data(self) -> np.ndarray:
|
||||
rng = np.random.default_rng(42)
|
||||
return (rng.standard_normal((300, 35)) * 0.3 + 0.5).astype(np.float32).clip(0, 1)
|
||||
|
||||
def test_returns_model_and_threshold(self, normal_data: np.ndarray) -> None:
|
||||
result = train_autoencoder(normal_data, epochs=5, batch_size=32)
|
||||
assert "model" in result
|
||||
assert "threshold" in result
|
||||
assert "scaler" in result
|
||||
assert "history" in result
|
||||
|
||||
def test_threshold_is_positive(self, normal_data: np.ndarray) -> None:
|
||||
result = train_autoencoder(normal_data, epochs=5, batch_size=32)
|
||||
assert result["threshold"] > 0.0
|
||||
|
||||
def test_history_has_train_loss(self, normal_data: np.ndarray) -> None:
|
||||
result = train_autoencoder(normal_data, epochs=5, batch_size=32)
|
||||
assert "train_loss" in result["history"]
|
||||
assert len(result["history"]["train_loss"]) == 5
|
||||
|
||||
def test_custom_percentile(self, normal_data: np.ndarray) -> None:
|
||||
result_95 = train_autoencoder(
|
||||
normal_data, epochs=3, batch_size=32, percentile=95.0
|
||||
)
|
||||
result_99 = train_autoencoder(
|
||||
normal_data, epochs=3, batch_size=32, percentile=99.0
|
||||
)
|
||||
assert result_99["threshold"] >= result_95["threshold"]
|
||||
|
||||
def test_model_is_in_eval_mode(self, normal_data: np.ndarray) -> None:
|
||||
result = train_autoencoder(normal_data, epochs=3, batch_size=32)
|
||||
assert not result["model"].training
|
||||
|
||||
|
||||
class TestRandomForestTraining:
|
||||
|
||||
@pytest.fixture
|
||||
def labeled_data(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
rng = np.random.default_rng(42)
|
||||
X = rng.standard_normal((400, 35)).astype(np.float32)
|
||||
y = np.concatenate([np.zeros(300, dtype=np.int64), np.ones(100, dtype=np.int64)])
|
||||
return X, y
|
||||
|
||||
def test_returns_model_and_metrics(
|
||||
self, labeled_data: tuple[np.ndarray, np.ndarray]
|
||||
) -> None:
|
||||
X, y = labeled_data
|
||||
result = train_random_forest(X, y)
|
||||
assert "model" in result
|
||||
assert "metrics" in result
|
||||
|
||||
def test_model_has_predict_proba(
|
||||
self, labeled_data: tuple[np.ndarray, np.ndarray]
|
||||
) -> None:
|
||||
X, y = labeled_data
|
||||
result = train_random_forest(X, y)
|
||||
assert hasattr(result["model"], "predict_proba")
|
||||
|
||||
def test_metrics_contain_required_keys(
|
||||
self, labeled_data: tuple[np.ndarray, np.ndarray]
|
||||
) -> None:
|
||||
X, y = labeled_data
|
||||
result = train_random_forest(X, y)
|
||||
for key in ("f1", "pr_auc", "accuracy", "precision", "recall"):
|
||||
assert key in result["metrics"]
|
||||
|
||||
def test_probabilities_in_valid_range(
|
||||
self, labeled_data: tuple[np.ndarray, np.ndarray]
|
||||
) -> None:
|
||||
X, y = labeled_data
|
||||
result = train_random_forest(X, y)
|
||||
proba = result["model"].predict_proba(X[:10])
|
||||
assert proba.min() >= 0.0
|
||||
assert proba.max() <= 1.0
|
||||
|
||||
def test_metrics_values_in_valid_range(
|
||||
self, labeled_data: tuple[np.ndarray, np.ndarray]
|
||||
) -> None:
|
||||
X, y = labeled_data
|
||||
result = train_random_forest(X, y)
|
||||
for value in result["metrics"].values():
|
||||
assert 0.0 <= value <= 1.0
|
||||
|
||||
|
||||
class TestIsolationForestTraining:
|
||||
|
||||
@pytest.fixture
|
||||
def normal_data(self) -> np.ndarray:
|
||||
rng = np.random.default_rng(42)
|
||||
return rng.standard_normal((200, 35)).astype(np.float32)
|
||||
|
||||
def test_returns_model(self, normal_data: np.ndarray) -> None:
|
||||
result = train_isolation_forest(normal_data)
|
||||
assert "model" in result
|
||||
|
||||
def test_model_has_score_samples(self, normal_data: np.ndarray) -> None:
|
||||
result = train_isolation_forest(normal_data)
|
||||
assert hasattr(result["model"], "score_samples")
|
||||
|
||||
def test_returns_metrics_with_n_samples(self, normal_data: np.ndarray) -> None:
|
||||
result = train_isolation_forest(normal_data)
|
||||
assert result["metrics"]["n_samples"] == 200
|
||||
|
||||
def test_anomaly_scores_distinguish_normal_and_outlier(
|
||||
self, normal_data: np.ndarray
|
||||
) -> None:
|
||||
result = train_isolation_forest(normal_data)
|
||||
model = result["model"]
|
||||
normal_scores = model.score_samples(normal_data[:50])
|
||||
outlier_data = np.full((50, 35), 10.0, dtype=np.float32)
|
||||
outlier_scores = model.score_samples(outlier_data)
|
||||
assert normal_scores.mean() > outlier_scores.mean()
|
||||
0
PROJECTS/advanced/Aenebris/.style.yapf → PROJECTS/advanced/haskell-reverse-proxy/.style.yapf
Executable file → Normal file
0
PROJECTS/advanced/Aenebris/.style.yapf → PROJECTS/advanced/haskell-reverse-proxy/.style.yapf
Executable file → Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Network Traffic Analyzer
|
||||
|
||||
Two implementations of the same network traffic analyzer — one in Python, one in C++. Both capture packets at the kernel level, parse protocol headers, and display real-time statistics.
|
||||
|
||||
## Implementations
|
||||
|
||||
| Implementation | Stack | Highlights |
|
||||
|---|---|---|
|
||||
| [**C++**](./cpp) | C++20 • libpcap • FTXUI | Interactive TUI, polymorphic IP parser, mutex-protected stats engine |
|
||||
| [**Python**](./python) | Python 3.14 • Scapy • Rich | Producer-consumer threading, BPF filter builder, Matplotlib chart export |
|
||||
|
||||
## Quick Start
|
||||
|
||||
**C++ — high-performance interactive TUI:**
|
||||
|
||||
```bash
|
||||
cd cpp
|
||||
./install.sh
|
||||
just run -i eth0
|
||||
```
|
||||
|
||||
**Python — scriptable with chart export:**
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv sync
|
||||
sudo netanal capture -i eth0
|
||||
```
|
||||
|
||||
Both require root or `CAP_NET_RAW` capability for packet capture.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# ©AngelaMos | 2026
|
||||
# .clang-format
|
||||
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: Always
|
||||
ColumnLimit: 120
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# ©AngelaMos | 2026
|
||||
# .clang-tidy
|
||||
|
||||
Checks: >
|
||||
bugprone-*, cert-*, clang-analyzer-*, cppcoreguidelines-*, performance-*,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cert-err33-c
|
||||
|
||||
WarningsAsErrors: "*"
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# ©AngelaMos | 2026
|
||||
# .gitignore
|
||||
|
||||
build/
|
||||
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.out
|
||||
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
*.cmake
|
||||
|
||||
compile_commands.json
|
||||
.cache/
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
|
@ -21,7 +21,7 @@ interfaces:
|
|||
|
||||
lint:
|
||||
@sed -i 's/-fdeps-format=p1689r5//g; s/-fmodule-mapper=[^ ]*//g; s/-fmodules-ts//g' build/release/compile_commands.json
|
||||
clang-tidy -p build/release src/**/*.cpp
|
||||
clang-tidy -p build/release src/**/*.cpp 2>/dev/null
|
||||
|
||||
format:
|
||||
find src include \( -name '*.cpp' -o -name '*.hpp' \) | xargs clang-format -i
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
```ruby
|
||||
▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄
|
||||
██ ██▄█▄ ██▀██ ██▄▄ ██▄▄ ██ ██▀▀▀ ██▀██ ███▄██ ██▀██ ██ ▀███▀ ▄█▀ ██▄▄ ██▄█▄
|
||||
██ ██ ██ ██▀██ ██ ██ ██ ▀████ ██▀██ ██ ▀██ ██▀██ ██▄▄▄ █ ▄██▄▄ ██▄▄▄ ██ ██
|
||||
|
||||
▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄
|
||||
██ ██▄█▄ ██▀██ ██▄▄ ██▄▄ ██ ██▀▀▀ ██▀██ ███▄██ ██▀██ ██ ▀███▀ ▄█▀ ██▄▄ ██▄█▄
|
||||
██ ██ ██ ██▀██ ██ ██ ██ ▀████ ██▀██ ██ ▀██ ██▀██ ██▄▄▄ █ ▄██▄▄ ██▄▄▄ ██ ██
|
||||
|
||||
```
|
||||
|
||||
>A high-performance CLI network analyzer built with libpcap for raw packet capture and FTXUI for a fully interactive terminal UI.
|
||||
The application captures packets directly from a network interface, parses protocol headers manually, aggregates statistics in real time
|
||||
|
||||
*Developed by [@deniskhud](https://github.com/deniskhud)*
|
||||
|
||||
---
|
||||

|
||||
|
||||
|
|
@ -26,9 +28,9 @@ Or grant capabilities:
|
|||
sudo setcap cap_net_raw,cap_net_admin=eip ./network-traffic-analyzer
|
||||
```
|
||||
|
||||
Or you can use `just` command
|
||||
Or you can use `just` command
|
||||
```
|
||||
just run
|
||||
just run
|
||||
```
|
||||
---
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ just run
|
|||
- Offline analysis from .pcap file (-r, --offline)
|
||||
- Packet count limit (-c)
|
||||
- Time limit for capture (-t)
|
||||
- Interface discovery (--interfaces)
|
||||
- Interface discovery (--interfaces)
|
||||
|
||||
> [!TIP]
|
||||
> For the complete list of CLI options, use:
|
||||
|
|
@ -63,7 +65,7 @@ just run
|
|||
- FTXUI
|
||||
- CMake
|
||||
|
||||
# Setup
|
||||
# Setup
|
||||
## 1. clone the repo then
|
||||
```bash
|
||||
cd network-traffic-analyzer
|
||||
|
|
@ -88,3 +90,15 @@ just run --offline traffic.pcap
|
|||
```
|
||||
just run --json result.json --csv result.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Learn More
|
||||
|
||||
| Doc | Contents |
|
||||
|-----|----------|
|
||||
| [00-OVERVIEW.md](./learn/00-OVERVIEW.md) | Quick start, prerequisites, project structure |
|
||||
| [01-CONCEPTS.md](./learn/01-CONCEPTS.md) | libpcap internals, BPF filters, protocol header parsing |
|
||||
| [02-ARCHITECTURE.md](./learn/02-ARCHITECTURE.md) | System design, component breakdown, data flow, threading model |
|
||||
| [03-IMPLEMENTATION.md](./learn/03-IMPLEMENTATION.md) | Line-by-line code walkthrough |
|
||||
| [04-CHALLENGES.md](./learn/04-CHALLENGES.md) | Extension ideas and advanced topics |
|
||||
|
|
|
|||
|
|
@ -9,17 +9,18 @@ class View {
|
|||
bool capture_finished, std::chrono::seconds timer);
|
||||
|
||||
private:
|
||||
ftxui::Element render_header(const StatsSnapshot &data, const std::string &interface, const std::string &filter);
|
||||
ftxui::Element render_stats(const StatsSnapshot &data);
|
||||
static ftxui::Element render_header(const StatsSnapshot &data, const std::string &interface,
|
||||
const std::string &filter);
|
||||
static ftxui::Element render_stats(const StatsSnapshot &data);
|
||||
|
||||
ftxui::Element render_transport(const StatsSnapshot &data);
|
||||
ftxui::Element render_application(const StatsSnapshot &data);
|
||||
ftxui::Element render_ip(const StatsSnapshot &data);
|
||||
ftxui::Element render_pairs(const StatsSnapshot &data);
|
||||
ftxui::Element render_bandwidth(const StatsSnapshot &data);
|
||||
ftxui::Element render_packets(const StatsSnapshot &data);
|
||||
static ftxui::Element render_transport(const StatsSnapshot &data);
|
||||
static ftxui::Element render_application(const StatsSnapshot &data);
|
||||
static ftxui::Element render_ip(const StatsSnapshot &data);
|
||||
static ftxui::Element render_pairs(const StatsSnapshot &data);
|
||||
static ftxui::Element render_bandwidth(const StatsSnapshot &data);
|
||||
static ftxui::Element render_packets(const StatsSnapshot &data);
|
||||
|
||||
ftxui::Element render_footer(bool capture_finished, std::chrono::seconds timer);
|
||||
static ftxui::Element render_footer(bool capture_finished, std::chrono::seconds timer);
|
||||
};
|
||||
|
||||
#endif // VIEW_HPP
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@
|
|||
class PcapCapture {
|
||||
private:
|
||||
/* libpcap error buffer */
|
||||
char errbuf[PCAP_ERRBUF_SIZE];
|
||||
char errbuf[PCAP_ERRBUF_SIZE] = {};
|
||||
|
||||
/* filter expression (compiled before capture) */
|
||||
std::string filter_exp = "";
|
||||
std::string filter_exp;
|
||||
/* compiled filter program (expression) */
|
||||
struct bpf_program fp = {};
|
||||
/* Active pcap handle */
|
||||
|
|
@ -73,7 +73,7 @@ class PcapCapture {
|
|||
* we use a static function and forward the call
|
||||
* to the class instance.
|
||||
*/
|
||||
static void callback(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
|
||||
static void callback(u_char *user, const struct pcap_pkthdr *header, const u_char *packet);
|
||||
// packet processing logic
|
||||
void got_packet(const struct pcap_pkthdr *header, const u_char *packet);
|
||||
|
||||
|
|
@ -81,10 +81,16 @@ class PcapCapture {
|
|||
std::thread thread;
|
||||
std::atomic<bool> running{false};
|
||||
void stop();
|
||||
Stats *stats;
|
||||
Stats *stats = nullptr;
|
||||
|
||||
public:
|
||||
PcapCapture() = default;
|
||||
~PcapCapture();
|
||||
|
||||
PcapCapture(const PcapCapture &) = delete;
|
||||
PcapCapture &operator=(const PcapCapture &) = delete;
|
||||
PcapCapture(PcapCapture &&) = delete;
|
||||
PcapCapture &operator=(PcapCapture &&) = delete;
|
||||
void print_interfaces();
|
||||
|
||||
bool isRunning() { return running; }
|
||||
|
|
|
|||
|
|
@ -72,11 +72,11 @@ class IPv6 : public IP_class {
|
|||
const ip6_hdr *ip_hdr = nullptr;
|
||||
int ip_hdr_len = 40;
|
||||
|
||||
in6_addr ip_source;
|
||||
in6_addr ip_dest;
|
||||
in6_addr ip_source = {};
|
||||
in6_addr ip_dest = {};
|
||||
|
||||
uint16_t src_port;
|
||||
uint16_t dest_port;
|
||||
uint16_t src_port = 0;
|
||||
uint16_t dest_port = 0;
|
||||
|
||||
const uint8_t *ptr = nullptr;
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ class Stats {
|
|||
return snapshot;
|
||||
}
|
||||
void update_bandwidth();
|
||||
double smooth_value(size_t i, size_t start);
|
||||
double smooth_bandwidth = 0.0;
|
||||
|
||||
void set_packets_limit(int limit) { limit_packets = limit; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
# C++ Network Traffic Analyzer
|
||||
|
||||
## What This Is
|
||||
|
||||
A C++20 CLI tool that captures live network traffic or reads offline pcap files, parses raw Ethernet/IP/TCP/UDP frames manually, and renders real-time statistics in a fully interactive terminal UI. Built with libpcap for kernel-level packet capture, Boost for CLI parsing, and FTXUI for the TUI.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Network visibility is how defenders catch attackers. If you can't see what's crossing your network, you can't detect intrusions, data exfiltration, or lateral movement. Tools like Wireshark, Zeek, and Suricata all sit on the same foundation: libpcap.
|
||||
|
||||
**Real-world scenarios where this applies:**
|
||||
|
||||
- **Incident response:** During the 2013 Target breach, 40 million card numbers were exfiltrated through POS systems over BlackPOS malware that used standard TCP connections to external IPs. Packet-level visibility would have flagged the unexpected outbound traffic from in-store register machines.
|
||||
|
||||
- **APT detection:** The 2020 SolarWinds attack (CVE-2020-10148) used HTTP beaconing — periodic check-ins from compromised hosts to attacker-controlled servers. Anomaly detection at the packet layer catches this: hosts that never talked to external IPs suddenly start.
|
||||
|
||||
- **Protocol baseline:** You cannot detect what's abnormal without first knowing what's normal. Packet analyzers establish baseline distributions — how much is DNS versus HTTPS versus SMTP — so unexpected shifts register as alerts.
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
**Security concepts:**
|
||||
|
||||
- **Raw socket access and capabilities** — Why packet capture requires root or `CAP_NET_RAW`, what Linux capabilities are, and how BPF (Berkeley Packet Filter) lets the kernel drop packets before they ever reach userspace.
|
||||
|
||||
- **Protocol header parsing** — How to walk the Ethernet frame → IP header → TCP/UDP header chain manually, using byte offsets and `reinterpret_cast`. This is what every IDS, DPI engine, and firewall does internally.
|
||||
|
||||
- **BPF filter expressions** — How to write and compile filters that run in the kernel (e.g., `tcp and port 443 and host 192.168.1.1`), why kernel-side filtering is orders of magnitude faster than userspace filtering.
|
||||
|
||||
**C++ patterns:**
|
||||
|
||||
- **Mutex-protected statistics** — Thread-safe aggregation with `std::mutex` and a lock-copy-return snapshot pattern that prevents data races without blocking the render thread.
|
||||
|
||||
- **Polymorphic IP parsing** — An abstract `IP_class` base with `IPv4` and `IPv6` subclasses, constructed from raw packet bytes and dispatching transport-layer parsing in the constructor.
|
||||
|
||||
- **RAII for C resources** — Wrapping a C-style `pcap_t*` handle in `std::unique_ptr<pcap_t, decltype(&pcap_close)>` so the handle is released automatically regardless of how the function exits.
|
||||
|
||||
- **FTXUI event loop threading** — Running packet capture in a background thread while FTXUI manages its own event loop on the main thread, coordinated with atomics and a shared render mutex.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Required:**
|
||||
|
||||
- **C++20 basics** — You need to read code using structured bindings, `std::format`, ranges, `std::atomic`, and lambdas. If `auto [key, val] : map` looks unfamiliar, review modern C++ first.
|
||||
|
||||
- **TCP/IP networking** — Know the Ethernet → IP → TCP/UDP layer stack. Understand what IP addresses and port numbers are, what a three-way handshake does, how ICMP differs from TCP.
|
||||
|
||||
- **Linux command line** — You'll run commands, inspect network interfaces with `ip link`, and grant capabilities with `setcap`. Basic shell navigation assumed.
|
||||
|
||||
**Tools you'll need:**
|
||||
|
||||
- **Linux (Ubuntu/Debian/Arch/Fedora)** — The capture engine uses Linux-specific headers (`netinet/tcp.h`, `netinet/ip.h`). macOS will work with minor changes; Windows will not.
|
||||
|
||||
- **Root or CAP_NET_RAW** — Packet capture requires this. Either run with `sudo` or grant the binary the capability: `sudo setcap cap_net_raw,cap_net_admin=eip ./network-traffic-analyzer`.
|
||||
|
||||
- **libpcap, Boost, Ninja, CMake** — Handled by `install.sh`.
|
||||
|
||||
**Helpful but not required:**
|
||||
|
||||
- **Wireshark experience** — If you've read pcap files or written BPF display filters, you'll recognize the concepts immediately. Not necessary to build the project.
|
||||
|
||||
- **Systems programming background** — Understanding virtual dispatch, vtables, and pointer arithmetic at the byte level will help you follow `IP.cpp`. Not required but accelerates comprehension.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/CarterPerez-dev/Cybersecurity-Projects.git
|
||||
cd PROJECTS/beginner/network-traffic-analyzer/cpp
|
||||
|
||||
# Install dependencies and build (one command)
|
||||
./install.sh
|
||||
|
||||
# List available interfaces
|
||||
just interfaces
|
||||
|
||||
# Live capture on eth0
|
||||
just capture -i eth0
|
||||
|
||||
# Capture 100 packets and export
|
||||
just run -i wlan0 -c 100 --json result.json
|
||||
|
||||
# Analyze a pcap file offline
|
||||
just run --offline traffic.pcap
|
||||
|
||||
# Run clang-tidy static analysis
|
||||
just lint
|
||||
|
||||
# Auto-format all source files
|
||||
just format
|
||||
```
|
||||
|
||||
Expected output: the TUI launches full-screen, showing a live-updating table of transport protocols, application protocols, top IPs, top source→destination pairs, and a bandwidth graph. Press `q` or `Escape` to exit. On exit, results export to JSON/CSV if flags were passed.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cpp/
|
||||
├── main.cpp # Entry point — arg parsing, TUI setup, thread coordination
|
||||
├── CMakeLists.txt # Build definition
|
||||
├── CMakePresets.json # Debug/release presets with compile_commands.json export
|
||||
├── Justfile # Dev commands (build, run, lint, format, clean)
|
||||
├── install.sh # One-command setup: deps + build
|
||||
├── .clang-tidy # Static analysis config
|
||||
├── .clang-format # Code style config
|
||||
├── include/
|
||||
│ ├── capture/pcapCapture.hpp # PcapCapture — libpcap wrapper
|
||||
│ ├── cli/
|
||||
│ │ ├── argsParse.hpp # argsParser — Boost.Program_options wrapper
|
||||
│ │ └── filter.hpp # filter struct + filter_type enum
|
||||
│ ├── packet/
|
||||
│ │ ├── packet.hpp # Packet struct, protocol enums
|
||||
│ │ └── IP.hpp # IP_class, IPv4, IPv6 declarations
|
||||
│ ├── stats/protocolStats.hpp # Stats class, StatsSnapshot, all stat structs
|
||||
│ └── TUI/view.hpp # View — FTXUI renderer
|
||||
└── src/
|
||||
├── capture/pcapCapture.cpp # Capture engine implementation
|
||||
├── cli/
|
||||
│ ├── argsParse.cpp # CLI option definitions
|
||||
│ └── filter.cpp # BPF filter builder
|
||||
├── packet/
|
||||
│ ├── packet.cpp # Application protocol identification
|
||||
│ └── IP.cpp # IPv4/IPv6 parsing implementation
|
||||
├── stats/protocolStats.cpp # Statistics aggregation, export
|
||||
└── TUI/view.cpp # TUI layout and rendering
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Understand the concepts** — Read [01-CONCEPTS.md](./01-CONCEPTS.md) for how libpcap, BPF, and protocol parsing work at the kernel level
|
||||
2. **Study the architecture** — Read [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) for the threading model and component design
|
||||
3. **Walk through the code** — Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) for line-by-line walkthroughs of every major component
|
||||
4. **Extend it** — Read [04-CHALLENGES.md](./04-CHALLENGES.md) for ideas on adding TCP stream reassembly, anomaly detection, and more
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Permission denied:**
|
||||
```
|
||||
pcap_open_live failed: eth0: You don't have permission to capture on that device
|
||||
```
|
||||
Run with `sudo just run` or grant capabilities: `sudo setcap cap_net_raw,cap_net_admin=eip ./build/release/network-traffic-analyzer`
|
||||
|
||||
**No packets on wireless interface:**
|
||||
Many Wi-Fi drivers don't pass all frames in managed mode. Try `lo` (loopback) first to verify the tool works, then try your wired interface.
|
||||
|
||||
**clang-tidy can't find compile_commands.json:**
|
||||
Run `just build` (debug preset) first — it generates `build/debug/compile_commands.json` with `CMAKE_EXPORT_COMPILE_COMMANDS=ON`. The `just lint` command reads from `build/release/compile_commands.json`, so run a release build too if needed.
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
# Concepts
|
||||
|
||||
## Packet Capture at the Kernel Level
|
||||
|
||||
When you open Wireshark and see packets, a lot has happened before the first byte reaches the screen. The OS kernel receives each frame off the network card, and normally only delivers frames addressed to your machine (or your subnet's broadcast address) to user processes. A packet sniffer needs everything — including frames addressed to other hosts.
|
||||
|
||||
This is promiscuous mode. When libpcap opens a device with `pcap_open_live(..., 1, ...)` (the `1` is the promiscuous flag, `pcapCapture.cpp:59`), it asks the kernel to pass all frames regardless of destination MAC address. The kernel network stack sees frames before the routing layer discards irrelevant ones.
|
||||
|
||||
The kernel copies matching frames from kernel space to a user-space ring buffer. Your program reads from that buffer via `pcap_loop()`. The copy is the bottleneck — that's why high-performance capture tools (like those used in data centers) use kernel bypass mechanisms like DPDK or XDP to skip the copy entirely.
|
||||
|
||||
## BPF — Berkeley Packet Filter
|
||||
|
||||
BPF is a small virtual machine that runs inside the kernel. When you pass a filter expression like `tcp and port 443`, libpcap compiles it to BPF bytecode and installs that program in the kernel. The kernel runs the BPF program on each frame before deciding whether to copy it to userspace.
|
||||
|
||||
The payoff: on a busy network, 99% of frames get dropped in the kernel without ever touching userspace. Moving filtering from user space to BPF reduced CPU usage from ~80% to ~5% in production network monitoring scenarios with port-specific filters.
|
||||
|
||||
In this project, `filter.cpp` builds BPF expression strings (`get_bpf_filter()`, line 27), and `pcapCapture.cpp` compiles and installs them via `pcap_compile()` + `pcap_setfilter()` (lines 68–75).
|
||||
|
||||
### Writing BPF Expressions
|
||||
|
||||
BPF syntax that pcap accepts:
|
||||
```
|
||||
tcp — only TCP traffic
|
||||
port 443 — source or destination port 443
|
||||
host 192.168.1.1 — to or from specific IP
|
||||
src host 10.0.0.1 — from specific source
|
||||
dst host 10.0.0.1 and port 80 — combined with AND
|
||||
tcp or udp — combined with OR
|
||||
```
|
||||
|
||||
The project's filter builder maps its own key:value syntax to BPF:
|
||||
- `protocol:https` → `port 443`
|
||||
- `ip:v4` → `ip`
|
||||
- `src:192.168.1.1` → `src host 192.168.1.1`
|
||||
- Multiple filters of the same type are ORed, different types are ANDed
|
||||
|
||||
## Ethernet Frames and Link Layer Types
|
||||
|
||||
Every packet on a physical network starts with a link-layer header. On Ethernet (the common case), that's a 14-byte Ethernet header: 6 bytes destination MAC, 6 bytes source MAC, 2 bytes EtherType.
|
||||
|
||||
But not every interface uses Ethernet headers. The Linux `any` pseudo-interface uses `DLT_LINUX_SLL` (a synthetic 16-byte header). Some environments use `DLT_LINUX_SLL2` (20-byte header). The offset before the IP layer differs by link type.
|
||||
|
||||
`pcapCapture.cpp:datalink_type()` (lines 13–39) handles this with a switch on the link type returned by `pcap_datalink()`. It sets both the `offset` (how many bytes to skip before the IP layer) and a `get_ether_type` lambda that extracts the EtherType field from the correct position.
|
||||
|
||||
```
|
||||
DLT_EN10MB → offset = 14, EtherType at bytes 12-13
|
||||
DLT_LINUX_SLL → offset = 16, protocol at bytes 14-15
|
||||
DLT_LINUX_SLL2 → offset = 20, protocol at bytes 18-19
|
||||
```
|
||||
|
||||
EtherType `0x0800` = IPv4, `0x86DD` = IPv6. The `got_packet()` function (line 152) reads the EtherType from the packet using `get_ether_type(packet)` and dispatches to `IPv4` or `IPv6` accordingly.
|
||||
|
||||
## Protocol Header Parsing
|
||||
|
||||
After skipping the link-layer header, the IP header starts at `packet + offset`. The parsing is raw pointer casting:
|
||||
|
||||
```cpp
|
||||
// IP.cpp:19 — cast raw bytes to ip header struct
|
||||
ip_hdr = reinterpret_cast<const ip *>(data);
|
||||
```
|
||||
|
||||
The `ip` struct from `<netinet/ip.h>` maps the fields at known byte offsets — `ip_hl` at bits 0–3 of byte 0 (the IP header length in 4-byte words), `ip_src` and `ip_dst` at bytes 12–15 and 16–19.
|
||||
|
||||
IP header length is `ip_hl * 4`. The minimum is 20 bytes (no options). IPv4.cpp validates this at line 25:
|
||||
```cpp
|
||||
if (ip_hdr_len < 20) throw std::runtime_error("Failed to initial IPv4 ");
|
||||
```
|
||||
|
||||
The transport header immediately follows the IP header:
|
||||
```cpp
|
||||
// IP.cpp:53 — walk past the IP header to reach TCP
|
||||
const auto *tcp = reinterpret_cast<const tcphdr *>(
|
||||
reinterpret_cast<const u_char *>(ip_hdr) + ip_hdr_len
|
||||
);
|
||||
```
|
||||
|
||||
The TCP header has its own variable length: `tcp->doff * 4` bytes (Data Offset field, minimum 20 bytes). The payload starts immediately after:
|
||||
```cpp
|
||||
// IP.cpp:58 — TCP payload pointer
|
||||
payload_ptr = reinterpret_cast<const u_char *>(tcp) + tcp->doff * 4;
|
||||
payload_len = ntohs(ip_hdr->ip_len) - (ip_hdr_len + tcp->doff * 4);
|
||||
```
|
||||
|
||||
Note the `reinterpret_cast<const u_char *>(tcp)` before the addition. Pointer arithmetic on a typed pointer advances by multiples of `sizeof(T)` — without the cast to byte pointer, `tcp + doff * 4` would advance by `doff * 4 * sizeof(tcphdr)` bytes, which is 20x too far.
|
||||
|
||||
## Application Protocol Identification
|
||||
|
||||
Application protocol detection uses two strategies, tried in order (`packet.cpp:4–57`):
|
||||
|
||||
**Payload inspection (deep packet inspection):** Check the first bytes of the payload against known magic values:
|
||||
- HTTP: first 4 bytes are `GET `, `POST`, `HEAD`, `PUT `, or `HTTP` (line 9)
|
||||
- TLS/HTTPS: first byte is `0x16` (TLS record type = handshake), second is `0x03` (version major) (line 18)
|
||||
|
||||
**Port-based fallback:** When payload is absent or unrecognized, check well-known ports:
|
||||
- TCP 22 → SSH, 25 → SMTP, 80 → HTTP, 443 → HTTPS
|
||||
- UDP 53 → DNS, 443 → QUIC, 123 → NTP
|
||||
|
||||
The protocol identification happens in the `Packet` constructor (packet.hpp:52):
|
||||
```cpp
|
||||
application_protocol = get_application_protocol();
|
||||
this->payload_ptr = nullptr; // null after identification — payload no longer needed
|
||||
```
|
||||
|
||||
Setting `payload_ptr = nullptr` after use is intentional. The pointer points into libpcap's internal ring buffer, which is only valid during the `pcap_loop` callback. Once the `Packet` is stored in `Stats.packets` deque, the pointer would be dangling. Nulling it makes this explicit.
|
||||
|
||||
## Thread Safety and the Snapshot Pattern
|
||||
|
||||
Two threads access the `Stats` object concurrently: the capture thread (calls `add_packet()`, `push()`) and the UI update thread (calls all the `update_*()` methods and `get_snapshot()`).
|
||||
|
||||
All `Stats` methods lock `mtx` at entry (`protocolStats.cpp:20`, `94`, `116`, `141`, etc.). Write operations complete under the lock. Reads via `get_snapshot()` (protocolStats.hpp:89) return a copy of the snapshot struct under the same lock:
|
||||
|
||||
```cpp
|
||||
StatsSnapshot get_snapshot() {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
return snapshot; // copy-on-exit
|
||||
}
|
||||
```
|
||||
|
||||
The FTXUI render lambda in `main.cpp` (line 92) reads from `current_render` under `render_mtx`, not from `Stats` directly. The UI update thread in `application_thread` (line 107) calls `get_snapshot()`, builds a new FTXUI element tree, stores it in `current_render` under `render_mtx`, then posts a custom event to trigger a repaint:
|
||||
|
||||
```cpp
|
||||
ftxui::Element new_frame = view.render(stats.get_snapshot(), ...);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_mtx);
|
||||
current_render = new_frame;
|
||||
}
|
||||
screen.PostEvent(ftxui::Event::Custom);
|
||||
```
|
||||
|
||||
This design means the FTXUI event loop thread never touches `Stats` directly — it only reads the pre-built `current_render` element.
|
||||
|
||||
## IPv6 Extension Headers
|
||||
|
||||
IPv6 dropped the options field from the fixed header and replaced it with extension headers — a chain of optional headers between the 40-byte base header and the transport layer. Each extension header has a `Next Header` field pointing to the next one in the chain.
|
||||
|
||||
The `IPv6` constructor (`IP.cpp:82–136`) walks this chain in a `while(true)` loop. At each iteration it reads the current header type and either dispatches to the transport handler (TCP/UDP/ICMP/ICMPV6/IGMP) and returns, or advances past a known extension header (Hop-by-Hop Options, Routing, Destination Options, Fragment) and continues:
|
||||
|
||||
```cpp
|
||||
case IPPROTO_HOPOPTS:
|
||||
case IPPROTO_ROUTING:
|
||||
case IPPROTO_DSTOPTS: {
|
||||
const auto *ext = reinterpret_cast<const ip6_ext *>(ptr);
|
||||
hdr = ext->ip6e_nxt;
|
||||
ptr += (ext->ip6e_len + 1) * 8; // len in 8-byte units, not counting first 8
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
The `(ext->ip6e_len + 1) * 8` arithmetic is the standard RFC 2460 formula: the length field counts 8-byte units excluding the first 8 bytes.
|
||||
|
||||
## Bandwidth Calculation
|
||||
|
||||
Bandwidth is computed once per second in `update_bandwidth()` (`protocolStats.cpp:230–252`):
|
||||
|
||||
```
|
||||
delta_bytes = total_bytes_now - total_bytes_last_tick
|
||||
bandwidth = delta_bytes / elapsed_seconds (bytes/sec)
|
||||
```
|
||||
|
||||
Raw bandwidth is noisy (bursty traffic, variable tick timing), so an exponential moving average smooths it:
|
||||
```cpp
|
||||
const double alpha = 0.2;
|
||||
smooth_bandwidth = alpha * snapshot.bandwidth + (1.0 - alpha) * smooth_bandwidth;
|
||||
```
|
||||
|
||||
An alpha of 0.2 gives recent samples 20% weight and the historical average 80% weight. Lower alpha = smoother but more lag. The smoothed value is stored in `bandwidth_history` for the TUI graph.
|
||||
|
||||
The TUI graph in `view.cpp:138–174` maps the last 50 bandwidth samples onto the graph widget's width using linear interpolation between samples — scaling each sample to a height value based on the current maximum bandwidth.
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
# System Architecture
|
||||
|
||||
## High-Level Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ main.cpp │
|
||||
│ • arg parsing • thread coordination │
|
||||
│ • offline vs live • CSV/JSON export on exit │
|
||||
└───────────┬─────────────────────────────┬───────────────────┘
|
||||
│ │
|
||||
┌──────▼──────┐ ┌────────▼────────┐
|
||||
│ PcapCapture │ │ FTXUI screen │
|
||||
│ (libpcap) │ │ (main thread) │
|
||||
└──────┬──────┘ └────────▲────────┘
|
||||
│ got_packet() │ PostEvent()
|
||||
│ ┌────────┴────────┐
|
||||
┌──────▼──────┐ │ application_ │
|
||||
│ IPv4/IPv6 │ │ thread │
|
||||
│ (parser) │ │ (UI update loop)│
|
||||
└──────┬──────┘ └────────▲────────┘
|
||||
│ Packet │ get_snapshot()
|
||||
┌──────▼──────────────────────────┐ │
|
||||
│ Stats │──┘
|
||||
│ add_packet() push() │
|
||||
│ transport_map application_map │
|
||||
│ ip_map pairs packets deque │
|
||||
│ bandwidth_history │
|
||||
│ StatsSnapshot (under mutex) │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Threading Model
|
||||
|
||||
There are three concurrent execution contexts:
|
||||
|
||||
**Capture thread** — spawned by `PcapCapture::start()` at `pcapCapture.cpp:80`. Runs `pcap_loop()` which calls `callback()` → `got_packet()` for each packet. Calls `Stats::add_packet()` and `Stats::push()`. Never touches the UI.
|
||||
|
||||
**UI update thread** (`application_thread`, `main.cpp:107`) — runs a loop that:
|
||||
1. Advances the elapsed timer
|
||||
2. Checks stop conditions (time limit, capture finished)
|
||||
3. Calls all `Stats::update_*()` methods to rebuild snapshot tables
|
||||
4. Calls `view.render(stats.get_snapshot(), ...)` to build a new FTXUI element tree
|
||||
5. Stores the element in `current_render` under `render_mtx`
|
||||
6. Posts a `Custom` event to the FTXUI screen to trigger a repaint
|
||||
|
||||
**FTXUI event loop** — runs on the main thread via `screen.Loop(component)` at `main.cpp:139`. The `Renderer` lambda (`main.cpp:92`) reads `current_render` under `render_mtx` and returns it. The `CatchEvent` lambda handles `q` and `Escape` to set `ui_running = false` and call `screen.Exit()`.
|
||||
|
||||
### Synchronization Points
|
||||
|
||||
| Shared resource | Protector | Access pattern |
|
||||
|---|---|---|
|
||||
| `Stats` internal maps | `Stats::mtx` | Capture thread writes; UI thread reads via update_* methods |
|
||||
| `StatsSnapshot` inside Stats | `Stats::mtx` | Both threads; snapshot is updated in place under lock |
|
||||
| `current_render` | `render_mtx` | UI thread writes; FTXUI renderer reads |
|
||||
| `capture_finished`, `ui_running` | `std::atomic<bool>` | Multiple threads read/write |
|
||||
| `timer` | `std::atomic<std::chrono::seconds>` | UI thread writes; render lambda reads |
|
||||
|
||||
## Components
|
||||
|
||||
### PcapCapture (`include/capture/pcapCapture.hpp`, `src/capture/pcapCapture.cpp`)
|
||||
|
||||
Wraps the entire libpcap lifecycle. Owns the pcap handle as a `unique_ptr<pcap_t, decltype(&pcap_close)>` so it's released on destruction regardless of how the object exits.
|
||||
|
||||
Key responsibilities:
|
||||
- `initialize()` — discover all network interfaces via `pcap_findalldevs()`
|
||||
- `datalink_type()` — detect the link-layer header type and set the byte offset + EtherType extractor
|
||||
- `start()` — open device in promiscuous mode, compile and install BPF filter, spawn capture thread
|
||||
- `start_offline()` — open pcap file, process synchronously (no extra thread)
|
||||
- `got_packet()` — parse each raw frame: extract EtherType, construct `IPv4` or `IPv6`, build `Packet`, forward to `Stats`
|
||||
- `~PcapCapture()` — calls `stop()`: breaks pcap loop, joins thread, frees filter program and interface list
|
||||
|
||||
The C-style `pcap_loop` callback requires a static function. `callback()` (line 132) uses the `user` pointer (which holds `this` cast to `u_char*`) to forward to the instance method `got_packet()`.
|
||||
|
||||
### IP_class / IPv4 / IPv6 (`include/packet/IP.hpp`, `src/packet/IP.cpp`)
|
||||
|
||||
Polymorphic IP header parser. `IP_class` is an abstract base declaring pure virtual transport handlers (`handle_tcp()`, `handle_udp()`, etc.). Both `IPv4` and `IPv6` inherit from it.
|
||||
|
||||
Parsing is constructor-based: both `IPv4(const u_char *data)` and `IPv6(const u_char *data)` accept a pointer to the IP header (already offset past the link-layer header) and complete all parsing in the constructor. After construction, the object exposes only pure accessors: `get_source()`, `get_dest()`, `get_src_port()`, `get_dest_port()`, `get_protocol()`, `get_payload_len()`, `get_payload_ptr()`.
|
||||
|
||||
IPv4 transport dispatch: `switch(ip_hdr->ip_p)` at line 28, dispatching to `handle_tcp/udp/icmp/icmpv6/igmp`.
|
||||
|
||||
IPv6 transport dispatch: a `while(true)` loop at line 94 that either handles a transport protocol (and returns) or walks past a known extension header and continues.
|
||||
|
||||
### Packet (`include/packet/packet.hpp`, `src/packet/packet.cpp`)
|
||||
|
||||
A value type holding everything extracted from a single frame:
|
||||
- `ip_version` — `v4` or `v6` (enum `IPVersion`)
|
||||
- `transport_protocol` — TCP/UDP/ICMP/ICMP6/IGMP/UNKNOWN (enum class `TransportProtocol`)
|
||||
- `application_protocol` — HTTP/HTTPS/DNS/SSH/etc (enum class `ApplicationProtocol`)
|
||||
- `src`, `dst` — IP addresses as strings
|
||||
- `src_port`, `dst_port` — port numbers
|
||||
- `total_len` — full frame length from pcap header
|
||||
- `payload_len` — transport payload length
|
||||
- `payload_ptr` — pointer into pcap's buffer, nulled after `get_application_protocol()` runs
|
||||
|
||||
The constructor computes `application_protocol` via `get_application_protocol()` (packet.cpp:4) then immediately nulls `payload_ptr`. This prevents callers from dereferencing a pointer that's only valid during the callback.
|
||||
|
||||
`get_application_protocol()` uses payload inspection first (HTTP verbs, TLS record header bytes), then falls back to port-based identification.
|
||||
|
||||
### Stats (`include/stats/protocolStats.hpp`, `src/stats/protocolStats.cpp`)
|
||||
|
||||
Thread-safe statistics engine. Internal state:
|
||||
- `transport_map` — `unordered_map<TransportProtocol, protocolStats>`
|
||||
- `application_map` — `unordered_map<ApplicationProtocol, protocolStats>`
|
||||
- `ip_map` — `unordered_map<string, IPStats>` (per-IP bidirectional counters)
|
||||
- `pairs` — `map<pair<string,string>, protocolStats>` (per src→dst pair)
|
||||
- `packets` — `deque<Packet>` (bounded ring of recent packets)
|
||||
- `snapshot` — `StatsSnapshot` (pre-built display rows, updated by `update_*` methods)
|
||||
- `bandwidth_history` — `vector<BandwidthPoint>` (time-series)
|
||||
|
||||
`add_packet()` (line 19) takes a lock and updates all raw maps in a single critical section. The `update_*()` methods take the lock, sort/format the data, and rebuild the corresponding `snapshot.*_rows` vectors. `get_snapshot()` returns a copy of the snapshot under the lock.
|
||||
|
||||
### View (`include/TUI/view.hpp`, `src/TUI/view.cpp`)
|
||||
|
||||
Stateless FTXUI layout composer. `render()` (view.cpp:5) builds the full terminal layout from a `StatsSnapshot`:
|
||||
|
||||
```
|
||||
┌─────── header ──────────────────────────────────────┐
|
||||
│ title | interface | filter │ traffic summary │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ transport table │ app table │ pairs table │ ← hbox, bordered
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ IP table (scrollable) │ bandwidth graph │ ← hbox
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ packets table (right panel, scrollable, width=100) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ footer: timer + exit hint │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
`render_bandwidth()` (line 138) defines a `GraphFunction` — a lambda that receives the graph widget's pixel dimensions and returns a `vector<int>` mapping each x-pixel to a y-height. It interpolates between the last 50 bandwidth samples and scales by `max_bandwidth`.
|
||||
|
||||
All table sections use `ftxui::Table` with header row styling (`DOUBLE` border on row 0, `LIGHT` on rest).
|
||||
|
||||
### Filter (`include/cli/filter.hpp`, `src/cli/filter.cpp`)
|
||||
|
||||
Two-function module:
|
||||
|
||||
`parse(str)` (filter.cpp:5) — splits a `key:value` string at the first `:`. Maps known key names (`protocol`, `port`, `src`, `dst`, `ip`) to the `filter_type` enum. Throws `std::invalid_argument` if no `:` is present.
|
||||
|
||||
`get_bpf_filter(filters)` (filter.cpp:27) — groups multiple filters by type into a `map<filter_type, vector<string>>`. Maps user-facing values to BPF syntax (e.g., `protocol:dns` → `port 53`, `ip:v4` → `ip`). Combines same-type filters with `or`, different types with `and`. Returns the resulting BPF expression string.
|
||||
|
||||
### argsParser (`include/cli/argsParse.hpp`, `src/cli/argsParse.cpp`)
|
||||
|
||||
Thin wrapper around `Boost.Program_options`. Defines all CLI options in the constructor and stores parsed results in a public `po::variables_map vm`. Options:
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-i`, `--interface` | `wlan0` | Network interface |
|
||||
| `-c`, `--count` | `0` (unlimited) | Packet count limit |
|
||||
| `--time`, `-t` | `INT_MAX` | Capture duration (seconds) |
|
||||
| `-r`, `--offline` | — | Read from pcap file |
|
||||
| `-f`, `--filter` | — | Filter expressions (composing, multiple) |
|
||||
| `-n`, `--limit` | `43` | Max displayed entries |
|
||||
| `--csv` / `--json` | — | Export paths |
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Live Capture
|
||||
|
||||
```
|
||||
User: just run -i eth0 -f protocol:tcp
|
||||
↓
|
||||
main.cpp:33-47 Parse args, build filter vector
|
||||
main.cpp:49 get_bpf_filter() → "tcp" BPF string
|
||||
main.cpp:54 capture.set_capabilities(interface, count, "tcp", limit, &stats)
|
||||
main.cpp:73 capture.start()
|
||||
↓
|
||||
pcapCapture.cpp:52-64 pcap_lookupnet → pcap_open_live (promiscuous, SNAP_LEN=1518)
|
||||
pcapCapture.cpp:64 datalink_type() → set offset + get_ether_type lambda
|
||||
pcapCapture.cpp:68-75 pcap_compile + pcap_setfilter (BPF "tcp" installed in kernel)
|
||||
pcapCapture.cpp:80-87 spawn thread → pcap_loop(callback)
|
||||
↓
|
||||
[Capture thread: per-packet]
|
||||
pcapCapture.cpp:132-136 callback() → got_packet()
|
||||
pcapCapture.cpp:158 get_ether_type(packet) → ETHERTYPE_IP or ETHERTYPE_IPV6
|
||||
pcapCapture.cpp:162 IPv4 ip(packet + offset) — constructor parses headers
|
||||
IP.cpp:18-50 extract src/dst, walk to TCP/UDP handler
|
||||
IP.cpp:52-61 handle_tcp: ports, payload_ptr, payload_len
|
||||
pcapCapture.cpp:165-168 Packet packetView(...) — constructor runs get_application_protocol()
|
||||
packet.cpp:4-57 memcmp payload bytes, port-based fallback
|
||||
payload_ptr = nullptr
|
||||
pcapCapture.cpp:167 stats->add_packet(packetView) — lock, update all maps
|
||||
pcapCapture.cpp:168 stats->push(packetView) — lock, push to deque
|
||||
↓
|
||||
[UI update thread: every loop iteration]
|
||||
main.cpp:117-121 update_transport_stats(), update_application_stats(),
|
||||
update_ip_stats(10), update_pairs(), update_bandwidth()
|
||||
main.cpp:124-126 view.render(stats.get_snapshot(), ...) → Element
|
||||
main.cpp:127-130 store in current_render under render_mtx, PostEvent to FTXUI
|
||||
↓
|
||||
[FTXUI main thread: on Custom event]
|
||||
main.cpp:92-95 Renderer lambda reads current_render under render_mtx → display
|
||||
```
|
||||
|
||||
### Offline Analysis
|
||||
|
||||
```
|
||||
main.cpp:61-70 capture.start_offline(pcap_file) — runs synchronously
|
||||
pcapCapture.cpp:199-211 pcap_open_offline → pcap_loop (no thread)
|
||||
[same per-packet flow as above]
|
||||
main.cpp:64-69 stats.update_packets() + update_application_stats() + ...
|
||||
↓
|
||||
main.cpp:86-88 view.render(stats.get_snapshot(), ...) → initial current_render
|
||||
screen.Loop() FTXUI displays static result (no UI update thread spawned)
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Decision: Constructor-Based Parsing vs Lazy Getters
|
||||
|
||||
The original `IP_class` design had `get_protocol()` as the entry point to all parsing — a getter with side effects. This creates hidden order dependencies: `get_src_port()` before `get_protocol()` returns 0 on IPv4 or garbage on IPv6.
|
||||
|
||||
The merged code (visible in the current `IPv4`/`IPv6` constructors) parses everything at construction time. Getters return already-computed values. No ordering requirements for callers.
|
||||
|
||||
### Decision: StatsSnapshot as Value Type
|
||||
|
||||
`StatsSnapshot` holds the pre-formatted display data as `vector<vector<string>>` rows. The UI thread calls `get_snapshot()` which copies this struct out under the mutex. The FTXUI renderer then works from its own copy with no need to hold any lock.
|
||||
|
||||
The alternative — having the renderer lock Stats directly — would mean the render mutex and the stats mutex interact, risking deadlock or blocking the capture thread on UI work.
|
||||
|
||||
### Decision: RAII Handle for pcap_t
|
||||
|
||||
`pcap_t*` is a C resource with `pcap_close()` as its destructor. Storing it as `unique_ptr<pcap_t, decltype(&pcap_close)>` means it's released automatically when `PcapCapture` is destroyed, even if exceptions fly. `handle.reset()` in `stop()` explicitly releases it early when the capture ends.
|
||||
|
||||
### Decision: Offset + Lambda for Link Types
|
||||
|
||||
Rather than if-chains scattered across `got_packet()`, the `datalink_type()` method sets both the `offset` integer and the `get_ether_type` function object once when the device opens. `got_packet()` stays clean: `uint16_t ether_type = get_ether_type(packet)`.
|
||||
|
||||
This makes adding a new link type a single `case` addition in `datalink_type()` rather than a change to the hot path.
|
||||
|
|
@ -0,0 +1,480 @@
|
|||
# Implementation Walkthrough
|
||||
|
||||
This document walks through every major component with exact file and line references. Read the source alongside this.
|
||||
|
||||
## Entry Point — `main.cpp`
|
||||
|
||||
### Initialization (lines 14–18)
|
||||
|
||||
```cpp
|
||||
Stats stats;
|
||||
PcapCapture capture;
|
||||
capture.initialize();
|
||||
argsParser parser(argc, argv);
|
||||
```
|
||||
|
||||
`Stats` default-constructs with all maps empty and `last_tick` set to now. `PcapCapture` initializes its `handle` with `nullptr`. `initialize()` calls `pcap_findalldevs()` to populate the `interfaces` linked list so `--interfaces` can print them.
|
||||
|
||||
### Argument Handling (lines 24–47)
|
||||
|
||||
```cpp
|
||||
if (parser.vm.contains("help")) { parser.print_help(); return 0; }
|
||||
if (parser.vm.contains("interfaces")) { capture.print_interfaces(); return 0; }
|
||||
```
|
||||
|
||||
Boost's `variables_map::contains()` checks if the flag was passed. Early-exit before any network setup.
|
||||
|
||||
```cpp
|
||||
std::vector<filter> filters;
|
||||
if (parser.vm.contains("filter")) {
|
||||
auto &f = parser.vm["filter"].as<std::vector<std::string>>();
|
||||
for (auto &x : f) { filters.push_back(parse(x)); filterString += x + " "; }
|
||||
}
|
||||
std::string expression = get_bpf_filter(filters);
|
||||
```
|
||||
|
||||
`--filter` is a composing option — it can appear multiple times and Boost accumulates all values into a `vector<string>`. Each `key:value` string is parsed to a `filter` struct, then `get_bpf_filter()` combines them into a single BPF expression.
|
||||
|
||||
### Offline vs Live Path (lines 51–74)
|
||||
|
||||
```cpp
|
||||
bool isOffline = parser.vm.contains("offline");
|
||||
capture.set_capabilities(interface, count, expression, limit, &stats);
|
||||
|
||||
if (isOffline) {
|
||||
capture.start_offline(parser.vm["offline"].as<std::string>());
|
||||
stats.update_packets();
|
||||
stats.update_application_stats();
|
||||
// ...all update methods
|
||||
}
|
||||
else {
|
||||
capture.start();
|
||||
}
|
||||
```
|
||||
|
||||
`set_capabilities()` stores the config inside `PcapCapture` and calls `stats->set_packets_limit(packets_limit)`. For offline mode, `start_offline()` runs synchronously and blocks until the entire file is processed — no threading. All stats are then computed once before the TUI launches. For live mode, `start()` spawns the capture thread and returns immediately.
|
||||
|
||||
### FTXUI Setup (lines 78–104)
|
||||
|
||||
```cpp
|
||||
auto screen = ftxui::ScreenInteractive::Fullscreen();
|
||||
View view;
|
||||
std::mutex render_mtx;
|
||||
ftxui::Element current_render = isOffline
|
||||
? view.render(stats.get_snapshot(), ...)
|
||||
: ftxui::text("Starting capture...");
|
||||
|
||||
auto component = ftxui::Renderer([&] {
|
||||
std::lock_guard<std::mutex> lock(render_mtx);
|
||||
return current_render;
|
||||
});
|
||||
|
||||
component |= ftxui::CatchEvent([&](ftxui::Event e) {
|
||||
if (e == ftxui::Event::Character('q') || e == ftxui::Event::Escape) {
|
||||
ui_running = false;
|
||||
screen.Exit();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
`current_render` holds the last-built element. The `Renderer` lambda is called by FTXUI's event loop on every repaint — it just returns whatever's in `current_render`, protected by `render_mtx`. The `CatchEvent` lambda intercepts keyboard events. Returning `true` means "event consumed, don't propagate".
|
||||
|
||||
Note: `return true` for all events (not just q/Esc) is intentional — it prevents FTXUI's default behavior from processing other keys.
|
||||
|
||||
### UI Update Thread (lines 106–137)
|
||||
|
||||
```cpp
|
||||
application_thread = std::thread([&] {
|
||||
while (!capture_finished && ui_running) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
timer.store(std::chrono::duration_cast<std::chrono::seconds>(now - begin));
|
||||
|
||||
if (timer.load() >= std::chrono::seconds(time) || !capture.isRunning())
|
||||
capture_finished = true;
|
||||
|
||||
stats.update_packets();
|
||||
stats.update_application_stats();
|
||||
stats.update_transport_stats();
|
||||
stats.update_ip_stats(10);
|
||||
stats.update_pairs();
|
||||
stats.update_bandwidth();
|
||||
|
||||
ftxui::Element new_frame = view.render(stats.get_snapshot(), ...);
|
||||
{ std::lock_guard<std::mutex> lock(render_mtx); current_render = new_frame; }
|
||||
if (ui_running) screen.PostEvent(ftxui::Event::Custom);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The loop updates stats, builds a new FTXUI element tree from the snapshot, swaps it into `current_render` (under lock), and tells the FTXUI event loop to repaint. `PostEvent(Custom)` is non-blocking — it enqueues the event and returns.
|
||||
|
||||
No explicit `sleep_for` in the loop (it's commented out at line 134). The loop runs as fast as the update methods complete, which is bounded by the stats mutex contention.
|
||||
|
||||
---
|
||||
|
||||
## PcapCapture — `src/capture/pcapCapture.cpp`
|
||||
|
||||
### `start()` (lines 50–88)
|
||||
|
||||
```cpp
|
||||
handle.reset(pcap_open_live(interface.c_str(), SNAP_LEN, 1, 1000, errbuf));
|
||||
```
|
||||
|
||||
- `SNAP_LEN = 1518` — maximum bytes to capture per packet (standard Ethernet MTU + headers)
|
||||
- `1` — promiscuous mode on
|
||||
- `1000` — read timeout in milliseconds (how long pcap_loop blocks waiting for packets)
|
||||
|
||||
```cpp
|
||||
datalink_type(pcap_datalink(handle.get()));
|
||||
```
|
||||
|
||||
Detects the link type and sets `offset` + `get_ether_type` before any packets arrive.
|
||||
|
||||
```cpp
|
||||
if (pcap_compile(handle.get(), &fp, filter_exp.c_str(), 0, net) == -1)
|
||||
throw std::runtime_error(...);
|
||||
if (pcap_setfilter(handle.get(), &fp) == -1)
|
||||
throw std::runtime_error(...);
|
||||
```
|
||||
|
||||
BPF compilation is into `struct bpf_program fp` (stack-allocated). After installation, the kernel runs this BPF program on every incoming frame.
|
||||
|
||||
```cpp
|
||||
thread = std::thread([this]() {
|
||||
if (pcap_loop(handle.get(), num_packets, &PcapCapture::callback, reinterpret_cast<u_char *>(this)) < 0) {}
|
||||
running = false;
|
||||
});
|
||||
```
|
||||
|
||||
`pcap_loop` blocks the thread until `num_packets` are captured (0 = unlimited) or `pcap_breakloop()` is called. `this` is passed as the `user` pointer — the static `callback` function casts it back to `PcapCapture*` to call `got_packet()`.
|
||||
|
||||
### `stop()` (lines 90–108)
|
||||
|
||||
```cpp
|
||||
pcap_freecode(&fp); // release BPF program memory
|
||||
if (!handle) return;
|
||||
running = false;
|
||||
pcap_breakloop(handle.get()); // signal pcap_loop to exit on next packet
|
||||
if (thread.joinable()) thread.join(); // wait for capture thread to finish
|
||||
handle.reset(); // calls pcap_close via unique_ptr deleter
|
||||
if (interfaces) { pcap_freealldevs(interfaces); interfaces = nullptr; }
|
||||
```
|
||||
|
||||
Called from the destructor. Order matters: break the loop first, then join, then close the handle.
|
||||
|
||||
### `got_packet()` (lines 152–179)
|
||||
|
||||
```cpp
|
||||
uint16_t ether_type = get_ether_type(packet);
|
||||
|
||||
if (ether_type == ETHERTYPE_IP) {
|
||||
IPv4 ip(packet + offset);
|
||||
TransportProtocol prot = ip.get_protocol();
|
||||
Packet packetView(v4, prot, ip.get_source(), ip.get_dest(),
|
||||
ip.get_src_port(), ip.get_dest_port(),
|
||||
header->len, ip.get_payload_len(), ip.get_payload_ptr());
|
||||
stats->add_packet(packetView);
|
||||
stats->push(packetView);
|
||||
}
|
||||
```
|
||||
|
||||
`packet + offset` skips past the link-layer header (14, 16, or 20 bytes depending on DLT type). `IPv4 ip(packet + offset)` parses all headers in the constructor. `Packet` construction calls `get_application_protocol()` and nulls `payload_ptr`. Then both `add_packet()` (updates all maps) and `push()` (pushes to the recent-packets deque) are called — both take the stats mutex internally.
|
||||
|
||||
---
|
||||
|
||||
## IP Parsing — `src/packet/IP.cpp`
|
||||
|
||||
### IPv4 Constructor (lines 18–50)
|
||||
|
||||
```cpp
|
||||
ip_hdr = reinterpret_cast<const ip *>(data);
|
||||
src = inet_ntoa(ip_hdr->ip_src); // converts in_addr to "a.b.c.d" string
|
||||
dst = inet_ntoa(ip_hdr->ip_dst);
|
||||
ip_hdr_len = ip_hdr->ip_hl * 4; // ip_hl is 4-bit field: header length in 32-bit words
|
||||
if (ip_hdr_len < 20) throw std::runtime_error("Failed to initial IPv4 ");
|
||||
```
|
||||
|
||||
`ip_hl * 4`: the Internet Header Length field is 4 bits, measured in 32-bit words. Minimum value is 5 (= 20 bytes, no options). Cast to bytes by multiplying by 4.
|
||||
|
||||
```cpp
|
||||
switch (ip_hdr->ip_p) {
|
||||
case IPPROTO_TCP: IPv4::handle_tcp(); break;
|
||||
case IPPROTO_UDP: IPv4::handle_udp(); break;
|
||||
case IPPROTO_ICMP: IPv4::handle_icmp(); break;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
`ip_p` is the Protocol field (byte 9 of the IP header). IANA assigns these: 6 = TCP, 17 = UDP, 1 = ICMP.
|
||||
|
||||
### IPv4 TCP Handler (lines 52–62)
|
||||
|
||||
```cpp
|
||||
const auto *tcp = reinterpret_cast<const tcphdr *>(
|
||||
reinterpret_cast<const u_char *>(ip_hdr) + ip_hdr_len
|
||||
);
|
||||
src_port = ntohs(tcp->source);
|
||||
dest_port = ntohs(tcp->dest);
|
||||
payload_ptr = reinterpret_cast<const u_char *>(tcp) + tcp->doff * 4;
|
||||
payload_len = ntohs(ip_hdr->ip_len) - (ip_hdr_len + tcp->doff * 4);
|
||||
```
|
||||
|
||||
`ip_hdr` points to the IP header. Adding `ip_hdr_len` bytes (cast to `u_char*` first for byte arithmetic) lands on the TCP header. `tcp->doff` is the TCP Data Offset: number of 32-bit words in the TCP header. `tcp->doff * 4` gives bytes. Payload starts after the TCP header.
|
||||
|
||||
`ntohs()` converts from network byte order (big-endian) to host byte order. All multi-byte fields in network protocols are big-endian.
|
||||
|
||||
### IPv6 Extension Header Walking (lines 82–136)
|
||||
|
||||
```cpp
|
||||
ptr = reinterpret_cast<const uint8_t *>(ip_hdr + 1); // past the 40-byte fixed header
|
||||
while (true) {
|
||||
switch (hdr) {
|
||||
case IPPROTO_TCP: IPv6::handle_tcp(); return;
|
||||
// ...
|
||||
case IPPROTO_HOPOPTS:
|
||||
case IPPROTO_ROUTING:
|
||||
case IPPROTO_DSTOPTS: {
|
||||
const auto *ext = reinterpret_cast<const ip6_ext *>(ptr);
|
||||
hdr = ext->ip6e_nxt;
|
||||
ptr += (ext->ip6e_len + 1) * 8;
|
||||
break;
|
||||
}
|
||||
case IPPROTO_FRAGMENT: {
|
||||
const auto *frag = reinterpret_cast<const ip6_frag *>(ptr);
|
||||
hdr = frag->ip6f_nxt;
|
||||
ptr += sizeof(ip6_frag);
|
||||
break;
|
||||
}
|
||||
default: protocol = TransportProtocol::UNKNOWN; return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ip_hdr + 1` — pointer arithmetic on `ip6_hdr*` advances by `sizeof(ip6_hdr) = 40` bytes, landing exactly at the first extension header or transport header.
|
||||
|
||||
`(ext->ip6e_len + 1) * 8` — RFC 2460 formula. `ip6e_len` is the length in 8-byte units not counting the first 8 bytes. So total bytes = `(len + 1) * 8`.
|
||||
|
||||
---
|
||||
|
||||
## Application Protocol Detection — `src/packet/packet.cpp`
|
||||
|
||||
### Two-Phase Identification (lines 4–57)
|
||||
|
||||
```cpp
|
||||
ApplicationProtocol Packet::get_application_protocol() {
|
||||
if (!payload_ptr || payload_len < 4) goto check_port;
|
||||
|
||||
if (transport_protocol == TransportProtocol::TCP) {
|
||||
if (!memcmp(payload_ptr, "GET ", 4) || !memcmp(payload_ptr, "POST", 4) || ...)
|
||||
return ApplicationProtocol::HTTP;
|
||||
}
|
||||
if ((src_port == 53 || dst_port == 53) && payload_len >= 12)
|
||||
return ApplicationProtocol::DNS;
|
||||
if (transport_protocol == TransportProtocol::TCP && payload_len >= 3) {
|
||||
if (payload_ptr[0] == 0x16 && payload_ptr[1] == 0x03)
|
||||
return ApplicationProtocol::HTTPS;
|
||||
}
|
||||
|
||||
check_port:
|
||||
uint16_t port = (src_port < dst_port) ? src_port : dst_port;
|
||||
// switch on port ...
|
||||
}
|
||||
```
|
||||
|
||||
Phase 1 — payload inspection. `memcmp(payload_ptr, "GET ", 4)` compares the first 4 payload bytes against the literal string. HTTP/1.x requests always start with a verb. TLS records start with `0x16 0x03` (Content-Type=Handshake, Version=3.x).
|
||||
|
||||
Phase 2 — port fallback via `goto check_port`. Using `goto` to jump past Phase 1 when payload is null or too short. `port = min(src_port, dst_port)` — for client→server connections, the server port is typically the well-known one and will be numerically smaller.
|
||||
|
||||
---
|
||||
|
||||
## Statistics Engine — `src/stats/protocolStats.cpp`
|
||||
|
||||
### `add_packet()` (lines 19–42)
|
||||
|
||||
```cpp
|
||||
void Stats::add_packet(const Packet &packet) {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
++snapshot.total_p;
|
||||
snapshot.total_b += packet.total_len;
|
||||
|
||||
auto &t = transport_map[packet.transport_protocol];
|
||||
t.packets++;
|
||||
t.bytes += packet.total_len;
|
||||
|
||||
auto &a = application_map[packet.application_protocol];
|
||||
a.packets++;
|
||||
a.bytes += packet.payload_len;
|
||||
|
||||
ip_map[packet.src].packets_sent++;
|
||||
ip_map[packet.src].bytes_sent += packet.total_len;
|
||||
ip_map[packet.dst].packets_received++;
|
||||
ip_map[packet.dst].bytes_received += packet.total_len;
|
||||
|
||||
auto key = std::make_pair(packet.src, packet.dst);
|
||||
pairs[key].packets++;
|
||||
pairs[key].bytes += packet.total_len;
|
||||
}
|
||||
```
|
||||
|
||||
`unordered_map::operator[]` default-constructs the value if the key doesn't exist. `protocolStats` has all members zero-initialized by default, so the first packet for any protocol inserts a zero-struct and then increments. Same pattern for `IPStats` and the `pairs` map.
|
||||
|
||||
Note: `total_p` and `total_b` are updated directly in `snapshot` (not in a separate struct) so they're immediately visible in `get_snapshot()` without an extra `update_*` call.
|
||||
|
||||
### `update_transport_stats()` (lines 93–107)
|
||||
|
||||
```cpp
|
||||
std::vector<std::pair<TransportProtocol, protocolStats>> tps(transport_map.begin(), transport_map.end());
|
||||
std::sort(tps.begin(), tps.end(), [](auto &a, auto &b) { return a.second.packets > b.second.packets; });
|
||||
```
|
||||
|
||||
Can't sort `unordered_map` in place — copy to a vector first, then sort. The lambda compares by packet count descending. Each row is then formatted with `std::format("{:.2f}", ...)` for MB and percentage values.
|
||||
|
||||
### `update_bandwidth()` (lines 230–252)
|
||||
|
||||
```cpp
|
||||
auto now = steady_clock::now();
|
||||
double elapsed = duration_cast<duration<double>>(now - last_tick).count();
|
||||
|
||||
if (elapsed >= 1.0) {
|
||||
uint32_t delta_bytes = snapshot.total_b - last_b;
|
||||
snapshot.bandwidth = delta_bytes / elapsed;
|
||||
last_b = snapshot.total_b;
|
||||
last_tick = now;
|
||||
|
||||
const double alpha = 0.2;
|
||||
smooth_bandwidth = alpha * snapshot.bandwidth + (1.0 - alpha) * smooth_bandwidth;
|
||||
snapshot.bandwidth_history.push_back({ts, smooth_bandwidth});
|
||||
}
|
||||
snapshot.max_bandwidth = std::max(snapshot.max_bandwidth, snapshot.bandwidth);
|
||||
```
|
||||
|
||||
Only samples once per second (when `elapsed >= 1.0`). `delta_bytes = current_total - last_snapshot_total`. Divided by elapsed seconds = bytes/sec. The EMA with alpha=0.2 smooths out spikes. `max_bandwidth` is updated every call (not just when a second has elapsed) so it tracks the actual peak.
|
||||
|
||||
---
|
||||
|
||||
## Filter Builder — `src/cli/filter.cpp`
|
||||
|
||||
### `parse()` (lines 5–25)
|
||||
|
||||
```cpp
|
||||
filter parse(const std::string &str) {
|
||||
auto pos = str.find(':');
|
||||
if (pos == std::string::npos)
|
||||
throw std::invalid_argument("Invalid filter format: '" + str + "' (expected key:value)");
|
||||
std::string type = str.substr(0, pos);
|
||||
std::string value = str.substr(pos + 1);
|
||||
if (type == "protocol") return {PROTOCOL, value};
|
||||
if (type == "port") return {PORT, value};
|
||||
if (type == "dest") return {IP_DEST, value};
|
||||
if (type == "src") return {IP_SRC, value};
|
||||
if (type == "ip") return {IP_TYPE, value};
|
||||
return {NONE, value};
|
||||
}
|
||||
```
|
||||
|
||||
`find(':')` returns `string::npos` (max value of `size_t`) if not found. The throw prevents the `npos + 1` unsigned overflow bug that would otherwise make `substr(npos + 1)` return the whole string as the value.
|
||||
|
||||
### `get_bpf_filter()` (lines 27–97)
|
||||
|
||||
```cpp
|
||||
std::map<filter_type, std::vector<std::string>> groups;
|
||||
for (const auto &x : f) {
|
||||
switch (x.type) {
|
||||
case PROTOCOL:
|
||||
if (x.val == "dns") groups[PROTOCOL].emplace_back("port 53");
|
||||
else if (x.val == "http") groups[PROTOCOL].emplace_back("port 80");
|
||||
// ...
|
||||
break;
|
||||
case IP_TYPE:
|
||||
if (x.val == "v4" || x.val == "4" || x.val == "ipv4") groups[IP_TYPE].emplace_back("ip");
|
||||
else if (x.val == "v6" || ...) groups[IP_TYPE].emplace_back("ip6");
|
||||
else throw std::invalid_argument("Unknown IP type: '" + x.val + "'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// combine: same-type = OR, different types = AND
|
||||
for (auto &[type, parts] : groups) {
|
||||
if (!first_group) result += " and ";
|
||||
if (parts.size() > 1) result += "(";
|
||||
for (size_t i = 0; i < parts.size(); ++i) {
|
||||
result += parts[i];
|
||||
if (i + 1 < parts.size()) result += " or ";
|
||||
}
|
||||
if (parts.size() > 1) result += ")";
|
||||
}
|
||||
```
|
||||
|
||||
`std::map` (ordered) ensures deterministic output order regardless of insertion order. The BPF AND/OR combination follows standard network filter semantics: same filter type with multiple values means "match any of these" (OR), while different filter types must all match (AND).
|
||||
|
||||
Example: `-f protocol:http -f protocol:https -f port:8080` → `(port 80 or port 443 or port 8080)`
|
||||
|
||||
---
|
||||
|
||||
## TUI Rendering — `src/TUI/view.cpp`
|
||||
|
||||
### Layout Composition (lines 5–47)
|
||||
|
||||
```cpp
|
||||
auto transport_section = hbox({
|
||||
render_transport(data) | flex,
|
||||
separator(),
|
||||
render_application(data) | flex,
|
||||
separator(),
|
||||
render_pairs(data) | flex,
|
||||
}) | border;
|
||||
|
||||
auto ip_section = hbox({
|
||||
render_ip(data) | border | size(HEIGHT, LESS_THAN, 10) | frame | vscroll_indicator,
|
||||
render_bandwidth(data) | border | flex
|
||||
});
|
||||
|
||||
auto right_panel = render_packets(data) | border | size(WIDTH, EQUAL, 100) | frame | vscroll_indicator;
|
||||
```
|
||||
|
||||
FTXUI uses a declarative layout model. `hbox` places elements side by side. `| flex` makes an element expand to fill available space. `| size(HEIGHT, LESS_THAN, 10)` caps height. `| frame | vscroll_indicator` adds scroll support.
|
||||
|
||||
`separator()` draws a vertical line between elements.
|
||||
|
||||
### Bandwidth Graph (lines 138–175)
|
||||
|
||||
```cpp
|
||||
GraphFunction fn = [this, data](int width, int height) {
|
||||
std::vector<int> output(width, 0);
|
||||
size_t n = data.bandwidth_history.size();
|
||||
size_t start = n > 50 ? n - 50 : 0; // last 50 samples
|
||||
|
||||
double max_bw = 1.0;
|
||||
for (size_t i = start; i < n; ++i)
|
||||
max_bw = std::max(max_bw, data.bandwidth_history[i].bytes_per_sec);
|
||||
|
||||
for (int x = 0; x < width; ++x) {
|
||||
double t = (double)x / (width - 1);
|
||||
double idx_f = start + t * (n - start - 1);
|
||||
size_t i0 = (size_t)idx_f;
|
||||
size_t i1 = std::min(i0 + 1, n - 1);
|
||||
double frac = idx_f - i0;
|
||||
double bw = data.bandwidth_history[i0].bytes_per_sec * (1.0 - frac)
|
||||
+ data.bandwidth_history[i1].bytes_per_sec * frac;
|
||||
output[x] = static_cast<int>(bw / max_bw * (height - 1));
|
||||
}
|
||||
return output;
|
||||
};
|
||||
```
|
||||
|
||||
The `GraphFunction` maps `width` screen columns to `height`-bounded integer heights. For each pixel column `x`, it computes a fractional index into the sample array (mapping the screen width to the sample range), interpolates linearly between adjacent samples, then normalizes to the graph height. `max_bw = 1.0` as the floor prevents division by zero when no traffic has been seen yet.
|
||||
|
||||
### Table Rendering (lines 83–93, pattern repeated for all tables)
|
||||
|
||||
```cpp
|
||||
Table table(data.transport_rows);
|
||||
table.SelectAll().Border(LIGHT);
|
||||
table.SelectRow(0).Decorate(bold);
|
||||
table.SelectRow(0).SeparatorVertical(LIGHT);
|
||||
table.SelectRow(0).Border(DOUBLE);
|
||||
return vbox({text("=== Transport protocols === ") | bold, table.Render()}) | flex;
|
||||
```
|
||||
|
||||
`data.transport_rows` is a `vector<vector<string>>` where row 0 is the header. `SelectAll().Border(LIGHT)` draws light borders around all cells. `SelectRow(0).Border(DOUBLE)` overrides the header row with a double border to visually distinguish it. `SelectRow(0).Decorate(bold)` makes header text bold.
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Challenges and Extensions
|
||||
|
||||
These are concrete things you can build on top of the existing codebase. Each one has a clear starting point in the code and a reason why it's worth doing.
|
||||
|
||||
## Beginner
|
||||
|
||||
### 1. Add ICMP Type Breakdown
|
||||
|
||||
Right now ICMP packets count as a single protocol entry. ICMP carries many message types: echo request/reply (ping), port unreachable, time exceeded (traceroute hops), etc.
|
||||
|
||||
**What to do:** In `IPv4::handle_icmp()` (`src/packet/IP.cpp:73`), cast the payload to `icmphdr` and read `type` and `code`. Extend `ApplicationProtocol` in `packet.hpp` to include `ICMP_ECHO`, `ICMP_UNREACHABLE`, etc., or add a separate `icmp_type` field to `Packet`.
|
||||
|
||||
**Why it matters:** ICMP is frequently used for recon (ping sweeps) and for covert channels (ICMP tunneling tools like `icmptunnel` encode data in the payload). Being able to distinguish echo requests from unreachable messages tells you whether you're being scanned or whether routes are broken.
|
||||
|
||||
### 2. Color-Code Protocols in the TUI
|
||||
|
||||
Add color to the transport and application protocol tables — TCP in blue, UDP in green, ICMP in yellow, unknown in red.
|
||||
|
||||
**What to do:** In `view.cpp:render_transport()`, access individual cells with `table.SelectCell(row, col).Decorate(color(Color::Blue))`. FTXUI's `Table::SelectCell()` takes row and column indices.
|
||||
|
||||
**Why it matters:** Security operators scan dashboards under time pressure. Color coding lets the eye jump to anomalies (unexpected UDP traffic, unknown protocols) without reading every row.
|
||||
|
||||
### 3. Add a Packet Rate Counter
|
||||
|
||||
Display packets/sec alongside the bandwidth graph. The bandwidth graph shows bytes/sec, but packet rate is separately useful — a flood of tiny packets at low bandwidth is a different pattern than a few large transfers.
|
||||
|
||||
**What to do:** Add `uint32_t last_p = 0` to `Stats` (alongside `last_b`). In `update_bandwidth()` (`protocolStats.cpp:230`), compute `delta_packets / elapsed` alongside the existing byte calculation. Add it to `StatsSnapshot` and display it in `render_header()` or `render_stats()`.
|
||||
|
||||
---
|
||||
|
||||
## Intermediate
|
||||
|
||||
### 4. TCP Stream Reassembly
|
||||
|
||||
Right now each TCP packet is analyzed independently. Application-level protocols that span multiple packets (HTTP responses, FTP transfers) aren't reconstructed. Reassembly combines TCP segments in sequence number order into a stream.
|
||||
|
||||
**What to do:** Add a `StreamTable` class that maps `(src_ip, dst_ip, src_port, dst_port)` to an ordered buffer of segments (keyed by sequence number). In `got_packet()` (`pcapCapture.cpp:152`), after constructing a TCP `Packet`, insert it into the stream table. When segments arrive in order, append to the stream buffer. When a gap fills, run application-layer identification on the complete buffer.
|
||||
|
||||
**Why it matters:** HTTP detection by verb matching (`memcmp(payload_ptr, "GET ", 4)` in `packet.cpp:9`) only works if the HTTP request fits in the first packet. For large PUT/POST bodies or slow connections, the verb might be in an earlier segment that's already been processed. Reassembly is how Snort, Suricata, and commercial DPI engines handle this.
|
||||
|
||||
### 5. DNS Query Logging
|
||||
|
||||
Extract DNS query names from UDP packets on port 53 and log them with timestamps.
|
||||
|
||||
**What to do:** In `Packet::get_application_protocol()` (`packet.cpp:14`), when DNS is identified, parse the DNS question section from `payload_ptr`. DNS is binary: bytes 0–11 are the header (ID, flags, counts), bytes 12+ are the question section as a length-prefixed label sequence (e.g., `\x03www\x07example\x03com\x00`). Walk the labels to extract the FQDN. Add a `dns_query` string field to `Packet` and display it in the packets table.
|
||||
|
||||
**Why it matters:** DNS is the phone book attackers always use. C2 beacons check in via DNS. Data exfiltration encodes data in DNS queries (DNS tunneling). A simple query log catches malware like `dnscat2` which uses DNS TXT records for a command shell — the queries show absurdly long subdomain names.
|
||||
|
||||
### 6. Alert Rules Engine
|
||||
|
||||
Add a rule evaluation engine that fires alerts when traffic matches configurable conditions: "alert if any single IP sends > 1000 packets in 10 seconds" (port scan), "alert if DNS queries/sec > 100" (tunneling), "alert if a new IP appears that wasn't in baseline" (lateral movement).
|
||||
|
||||
**What to do:** Create an `AlertRule` struct with a condition function and threshold. Create an `AlertEngine` that the UI update thread calls after `update_ip_stats()`. Rules inspect the `Stats` snapshot for threshold violations. Store triggered alerts in a `deque<Alert>` and add a panel to `view.cpp` to display them.
|
||||
|
||||
**Why it matters:** This is the core of what a SIEM does. SIEMs (Splunk, Elastic SIEM, IBM QRadar) correlate events from multiple sources, but the underlying idea — evaluate rule conditions against observed metrics, fire alert when exceeded — is what you're building here.
|
||||
|
||||
---
|
||||
|
||||
## Advanced
|
||||
|
||||
### 7. Packet Payload Hex Dump
|
||||
|
||||
Add a detail view that shows the raw hex bytes and ASCII representation of a selected packet's payload — like Wireshark's bottom pane.
|
||||
|
||||
**What to do:** The `payload_ptr` is currently nulled after `get_application_protocol()` runs (`packet.hpp:53`). To support hex dump, copy the payload bytes into a `vector<uint8_t>` before nulling. Add a selected-packet index to `View` state, a way to navigate it (arrow keys via `CatchEvent`), and a `render_hex_dump()` method that formats bytes as `XX XX XX ... | .text..` rows.
|
||||
|
||||
FTXUI doesn't have a built-in hex dump widget. You'd build it with a series of `hbox({text(hex_col) | fixed(50), text(ascii_col)})` elements.
|
||||
|
||||
**Why it matters:** Payload inspection is how you verify that an alert is real. "DNS traffic" is easy to detect. Knowing whether that DNS traffic contains normal queries or base64-encoded exfiltration requires looking at the bytes.
|
||||
|
||||
### 8. Anomaly Baseline and Deviation Detection
|
||||
|
||||
Record a traffic baseline over a configurable window (e.g., first 60 seconds), then flag deviations. A new protocol appearing, a normally-quiet IP becoming a top talker, or TCP traffic on a UDP-only port all warrant investigation.
|
||||
|
||||
**What to do:** Add a `Baseline` class that snapshots `transport_map` and `ip_map` after the baseline window. Add a `compare(current_snapshot, baseline)` function that computes Z-scores or percentage deltas for each metric. Store deviations in a `vector<Deviation>` and render them in a new TUI panel.
|
||||
|
||||
**Why it matters:** The 2020 SolarWinds attack persisted undetected for 9 months partly because the malicious traffic mimicked normal Orion product telemetry — it looked like expected traffic to anyone checking manually. Automated baseline comparison would have flagged the new beacon pattern against the pre-compromise baseline.
|
||||
|
||||
### 9. PCAP Export During Live Capture
|
||||
|
||||
Let the user write a `.pcap` file of captured traffic in real time, not just export stats at the end. This enables offline analysis in Wireshark after the fact.
|
||||
|
||||
**What to do:** Use `pcap_dump_open()` to create a dump file handle, and `pcap_dump()` inside `got_packet()` (`pcapCapture.cpp:152`) to write each raw packet with its `pcap_pkthdr`. Add a `--write` flag to `argsParser` (`argsParse.cpp`). The dump handle is another C resource that benefits from RAII wrapping.
|
||||
|
||||
**Why it matters:** Live capture tools in incident response always write pcap files. You want to capture first, analyze later — especially if the attack is still in progress. Tools like `tcpdump -w` and `dumpcap` do exactly this.
|
||||
|
||||
### 10. Protocol-Aware Port Scanning Detection
|
||||
|
||||
Detect SYN-only streams (where TCP connections are initiated but never completed) and flag them as potential port scans.
|
||||
|
||||
**What to do:** In the TCP stream table (from challenge 4), track TCP flags on each packet. A SYN with no SYN-ACK reply within a timeout is a half-open scan. Count the number of distinct destination ports from a single source IP within a time window — more than N unique ports in M seconds = probable scan. This is the algorithm `portsentry`, `snort`, and modern firewalls use.
|
||||
|
||||
The 2016 Mirai botnet scanned the entire IPv4 internet for open Telnet/SSH ports in under an hour by running SYN scans from 100,000+ infected devices simultaneously. Detecting scan patterns at the packet level — not just counting connection attempts — is how network IDS systems catch this.
|
||||
|
|
@ -137,33 +137,34 @@ ftxui::Element View::render_pairs(const StatsSnapshot &data) {
|
|||
|
||||
ftxui::Element View::render_bandwidth(const StatsSnapshot &data) {
|
||||
|
||||
GraphFunction fn = [this, data](int width, int height) {
|
||||
GraphFunction fn = [data](int width, int height) {
|
||||
std::vector<int> output(width, 0);
|
||||
|
||||
if (data.bandwidth_history.size() < 2)
|
||||
if (data.bandwidth_history.size() < 2) {
|
||||
return output;
|
||||
}
|
||||
|
||||
size_t n = data.bandwidth_history.size();
|
||||
size_t start = n > 50 ? n - 50 : 0;
|
||||
const size_t n = data.bandwidth_history.size();
|
||||
const size_t start = n > 50 ? n - 50 : 0;
|
||||
|
||||
double max_bw = 1.0;
|
||||
for (size_t i = start; i < n; ++i)
|
||||
for (size_t i = start; i < n; ++i) {
|
||||
max_bw = std::max(max_bw, data.bandwidth_history[i].bytes_per_sec);
|
||||
}
|
||||
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const double t = static_cast<double>(x) / static_cast<double>(width - 1);
|
||||
|
||||
double t = (double)x / (width - 1);
|
||||
const double idx_f = static_cast<double>(start) + t * static_cast<double>(n - start - 1);
|
||||
const size_t i0 = static_cast<size_t>(idx_f);
|
||||
const size_t i1 = std::min(i0 + 1, n - 1);
|
||||
|
||||
double idx_f = start + t * (n - start - 1);
|
||||
size_t i0 = (size_t)idx_f;
|
||||
size_t i1 = std::min(i0 + 1, n - 1);
|
||||
const double frac = idx_f - static_cast<double>(i0);
|
||||
const double bw = data.bandwidth_history[i0].bytes_per_sec * (1.0 - frac) +
|
||||
data.bandwidth_history[i1].bytes_per_sec * frac;
|
||||
|
||||
double frac = idx_f - i0;
|
||||
double bw = data.bandwidth_history[i0].bytes_per_sec * (1.0 - frac) +
|
||||
data.bandwidth_history[i1].bytes_per_sec * frac;
|
||||
|
||||
double v = bw / max_bw;
|
||||
output[x] = static_cast<int>(v * (height - 1));
|
||||
const double v = bw / max_bw;
|
||||
output[x] = static_cast<int>(v * static_cast<double>(height - 1));
|
||||
}
|
||||
|
||||
return output;
|
||||
|
|
@ -185,4 +186,4 @@ ftxui::Element View::render_packets(const StatsSnapshot &data) {
|
|||
|
||||
table.Render() | flex}) |
|
||||
flex;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ void PcapCapture::datalink_type(int type) {
|
|||
case DLT_EN10MB: {
|
||||
offset = 14;
|
||||
get_ether_type = [](const u_char *p) {
|
||||
auto *eth = reinterpret_cast<const ether_header *>(p);
|
||||
const auto *eth = reinterpret_cast<const ether_header *>(p);
|
||||
return ntohs(eth->ether_type);
|
||||
};
|
||||
break;
|
||||
|
|
@ -89,19 +89,21 @@ void PcapCapture::start() {
|
|||
PcapCapture::~PcapCapture() { stop(); }
|
||||
void PcapCapture::stop() {
|
||||
pcap_freecode(&fp);
|
||||
if (!handle)
|
||||
if (!handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
running = false;
|
||||
|
||||
pcap_breakloop(handle.get());
|
||||
|
||||
if (thread.joinable())
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
handle.reset();
|
||||
|
||||
if (interfaces) {
|
||||
if (interfaces != nullptr) {
|
||||
pcap_freealldevs(interfaces);
|
||||
interfaces = nullptr;
|
||||
}
|
||||
|
|
@ -110,9 +112,9 @@ void PcapCapture::stop() {
|
|||
/* print all available interfaces */
|
||||
void PcapCapture::print_interfaces() {
|
||||
int i = 0;
|
||||
for (pcap_if_t *dev = interfaces; dev; dev = dev->next) {
|
||||
for (pcap_if_t *dev = interfaces; dev != nullptr; dev = dev->next) {
|
||||
printf("%d. %s ", ++i, dev->name);
|
||||
if (dev->description) {
|
||||
if (dev->description != nullptr) {
|
||||
printf("(%s)\n", dev->description);
|
||||
} else {
|
||||
printf("\n");
|
||||
|
|
@ -130,9 +132,10 @@ void PcapCapture::print_interfaces() {
|
|||
* @param packet Raw packet bytes
|
||||
*/
|
||||
void PcapCapture::callback(u_char *user, const struct pcap_pkthdr *header, const u_char *packet) {
|
||||
auto *self = reinterpret_cast<PcapCapture *>(user);
|
||||
if (!self->isRunning())
|
||||
auto * const self = reinterpret_cast<PcapCapture *>(user);
|
||||
if (!self->isRunning()) {
|
||||
return;
|
||||
}
|
||||
self->got_packet(header, packet);
|
||||
}
|
||||
|
||||
|
|
@ -150,17 +153,17 @@ void PcapCapture::callback(u_char *user, const struct pcap_pkthdr *header, const
|
|||
* Other Ethernet types are ignored.
|
||||
*/
|
||||
void PcapCapture::got_packet(const struct pcap_pkthdr *header, const u_char *packet) {
|
||||
if (!running)
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Ethernet header ---
|
||||
// const auto* ethernet = reinterpret_cast<const ether_header*>(packet + offset);
|
||||
uint16_t ether_type = get_ether_type(packet);
|
||||
const uint16_t ether_type = get_ether_type(packet);
|
||||
|
||||
/* if we have a ipv4 type */
|
||||
if (ether_type == ETHERTYPE_IP) {
|
||||
IPv4 ip(packet + offset);
|
||||
TransportProtocol prot = ip.get_protocol();
|
||||
const TransportProtocol prot = ip.get_protocol();
|
||||
|
||||
Packet packetView(v4, prot, ip.get_source(), ip.get_dest(), ip.get_src_port(), ip.get_dest_port(), header->len,
|
||||
ip.get_payload_len(), ip.get_payload_ptr());
|
||||
|
|
@ -170,7 +173,7 @@ void PcapCapture::got_packet(const struct pcap_pkthdr *header, const u_char *pac
|
|||
/* ipv6 type */
|
||||
else if (ether_type == ETHERTYPE_IPV6) {
|
||||
IPv6 ip(packet + offset);
|
||||
TransportProtocol prot = ip.get_protocol();
|
||||
const TransportProtocol prot = ip.get_protocol();
|
||||
Packet packetView(v6, prot, ip.get_source(), ip.get_dest(), ip.get_src_port(), ip.get_dest_port(), header->len,
|
||||
ip.get_payload_len(), ip.get_payload_ptr());
|
||||
stats->add_packet(packetView);
|
||||
|
|
@ -209,4 +212,4 @@ void PcapCapture::start_offline(const std::string &fpath) {
|
|||
pcap_loop(handle.get(), num_packets, &PcapCapture::callback, reinterpret_cast<u_char *>(this));
|
||||
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,25 +3,30 @@
|
|||
#include <stdexcept>
|
||||
|
||||
filter parse(const std::string &str) {
|
||||
auto pos = str.find(':');
|
||||
const auto pos = str.find(':');
|
||||
if (pos == std::string::npos) {
|
||||
throw std::invalid_argument("Invalid filter format: '" + str + "' (expected key:value)");
|
||||
}
|
||||
|
||||
std::string type = str.substr(0, pos);
|
||||
std::string value = str.substr(pos + 1);
|
||||
const std::string type = str.substr(0, pos);
|
||||
const std::string value = str.substr(pos + 1);
|
||||
|
||||
if (type == "protocol")
|
||||
return {PROTOCOL, value};
|
||||
if (type == "port")
|
||||
return {PORT, value};
|
||||
if (type == "dest")
|
||||
return {IP_DEST, value};
|
||||
if (type == "src")
|
||||
return {IP_SRC, value};
|
||||
if (type == "ip")
|
||||
return {IP_TYPE, value};
|
||||
return {NONE, value};
|
||||
if (type == "protocol") {
|
||||
return {.type = PROTOCOL, .val = value};
|
||||
}
|
||||
if (type == "port") {
|
||||
return {.type = PORT, .val = value};
|
||||
}
|
||||
if (type == "dest") {
|
||||
return {.type = IP_DEST, .val = value};
|
||||
}
|
||||
if (type == "src") {
|
||||
return {.type = IP_SRC, .val = value};
|
||||
}
|
||||
if (type == "ip") {
|
||||
return {.type = IP_TYPE, .val = value};
|
||||
}
|
||||
return {.type = NONE, .val = value};
|
||||
}
|
||||
|
||||
std::string get_bpf_filter(const std::vector<filter> &f) {
|
||||
|
|
@ -30,20 +35,21 @@ std::string get_bpf_filter(const std::vector<filter> &f) {
|
|||
for (const auto &x : f) {
|
||||
switch (x.type) {
|
||||
case PROTOCOL:
|
||||
if (x.val == "dns")
|
||||
if (x.val == "dns") {
|
||||
groups[PROTOCOL].emplace_back("port 53");
|
||||
else if (x.val == "http")
|
||||
} else if (x.val == "http") {
|
||||
groups[PROTOCOL].emplace_back("port 80");
|
||||
else if (x.val == "https")
|
||||
} else if (x.val == "https") {
|
||||
groups[PROTOCOL].emplace_back("port 443");
|
||||
else if (x.val == "ssh")
|
||||
} else if (x.val == "ssh") {
|
||||
groups[PROTOCOL].emplace_back("port 22");
|
||||
else if (x.val == "ftp")
|
||||
} else if (x.val == "ftp") {
|
||||
groups[PROTOCOL].emplace_back("port 21");
|
||||
else if (x.val == "smtp")
|
||||
} else if (x.val == "smtp") {
|
||||
groups[PROTOCOL].emplace_back("port 25");
|
||||
else
|
||||
} else {
|
||||
groups[PROTOCOL].push_back(x.val);
|
||||
}
|
||||
break;
|
||||
|
||||
case IP_DEST:
|
||||
|
|
@ -58,12 +64,13 @@ std::string get_bpf_filter(const std::vector<filter> &f) {
|
|||
groups[PORT].push_back("port " + x.val);
|
||||
break;
|
||||
case IP_TYPE: {
|
||||
if (x.val == "v4" || x.val == "4" || x.val == "ipv4")
|
||||
if (x.val == "v4" || x.val == "4" || x.val == "ipv4") {
|
||||
groups[IP_TYPE].emplace_back("ip");
|
||||
else if (x.val == "v6" || x.val == "6" || x.val == "ipv6")
|
||||
} else if (x.val == "v6" || x.val == "6" || x.val == "ipv6") {
|
||||
groups[IP_TYPE].emplace_back("ip6");
|
||||
else
|
||||
} else {
|
||||
throw std::invalid_argument("Unknown IP type: '" + x.val + "'");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -76,21 +83,25 @@ std::string get_bpf_filter(const std::vector<filter> &f) {
|
|||
bool first_group = true;
|
||||
|
||||
for (auto &[type, parts] : groups) {
|
||||
if (!first_group)
|
||||
if (!first_group) {
|
||||
result += " and ";
|
||||
}
|
||||
first_group = false;
|
||||
|
||||
if (parts.size() > 1)
|
||||
if (parts.size() > 1) {
|
||||
result += "(";
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < parts.size(); ++i) {
|
||||
result += parts[i];
|
||||
if (i + 1 < parts.size())
|
||||
if (i + 1 < parts.size()) {
|
||||
result += " or ";
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.size() > 1)
|
||||
if (parts.size() > 1) {
|
||||
result += ")";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -15,13 +15,16 @@ std::string IP_class::get_source() { return src; }
|
|||
std::string IP_class::get_dest() { return dst; }
|
||||
|
||||
/*** Ipv4 ***/
|
||||
IPv4::IPv4(const u_char *data) {
|
||||
ip_hdr = reinterpret_cast<const ip *>(data);
|
||||
IPv4::IPv4(const u_char *data)
|
||||
: ip_hdr(reinterpret_cast<const ip *>(data)),
|
||||
ip_hdr_len(static_cast<int>(ip_hdr->ip_hl) * 4) {
|
||||
std::array<char, INET_ADDRSTRLEN> src_buf{};
|
||||
inet_ntop(AF_INET, &ip_hdr->ip_src, src_buf.data(), sizeof(src_buf));
|
||||
src = src_buf.data();
|
||||
|
||||
src = inet_ntoa(ip_hdr->ip_src);
|
||||
dst = inet_ntoa(ip_hdr->ip_dst);
|
||||
|
||||
ip_hdr_len = ip_hdr->ip_hl * 4;
|
||||
std::array<char, INET_ADDRSTRLEN> dst_buf{};
|
||||
inet_ntop(AF_INET, &ip_hdr->ip_dst, dst_buf.data(), sizeof(dst_buf));
|
||||
dst = dst_buf.data();
|
||||
if (ip_hdr_len < 20) {
|
||||
throw std::runtime_error("Failed to initial IPv4 ");
|
||||
}
|
||||
|
|
@ -55,8 +58,9 @@ void IPv4::handle_tcp() {
|
|||
src_port = ntohs(tcp->source);
|
||||
dest_port = ntohs(tcp->dest);
|
||||
|
||||
payload_ptr = reinterpret_cast<const u_char *>(tcp) + tcp->doff * 4;
|
||||
payload_len = ntohs(ip_hdr->ip_len) - (ip_hdr_len + tcp->doff * 4);
|
||||
const auto tcp_hdr_len = static_cast<std::size_t>(tcp->doff) * 4U;
|
||||
payload_ptr = reinterpret_cast<const u_char *>(tcp) + tcp_hdr_len;
|
||||
payload_len = static_cast<uint16_t>(ntohs(ip_hdr->ip_len) - ip_hdr_len - static_cast<int>(tcp_hdr_len));
|
||||
|
||||
protocol = TransportProtocol::TCP;
|
||||
}
|
||||
|
|
@ -79,8 +83,9 @@ uint16_t IPv4::get_dest_port() { return dest_port; }
|
|||
|
||||
/*** Ipv6 ***/
|
||||
|
||||
IPv6::IPv6(const u_char *data) {
|
||||
ip_hdr = reinterpret_cast<const ip6_hdr *>(data);
|
||||
IPv6::IPv6(const u_char *data)
|
||||
: ip_hdr(reinterpret_cast<const ip6_hdr *>(data)),
|
||||
ptr(reinterpret_cast<const uint8_t *>(ip_hdr + 1)) {
|
||||
uint8_t hdr = ip_hdr->ip6_nxt;
|
||||
std::array<char, INET6_ADDRSTRLEN> src{};
|
||||
inet_ntop(AF_INET6, &ip_hdr->ip6_src, src.data(), sizeof(src));
|
||||
|
|
@ -89,8 +94,6 @@ IPv6::IPv6(const u_char *data) {
|
|||
std::array<char, INET6_ADDRSTRLEN> dst{};
|
||||
inet_ntop(AF_INET6, &ip_hdr->ip6_dst, dst.data(), sizeof(dst));
|
||||
this->dst = dst.data();
|
||||
|
||||
ptr = reinterpret_cast<const uint8_t *>(ip_hdr + 1);
|
||||
while (true) {
|
||||
switch (hdr) {
|
||||
case IPPROTO_TCP:
|
||||
|
|
@ -117,7 +120,7 @@ IPv6::IPv6(const u_char *data) {
|
|||
case IPPROTO_DSTOPTS: {
|
||||
const auto *ext = reinterpret_cast<const ip6_ext *>(ptr);
|
||||
hdr = ext->ip6e_nxt;
|
||||
ptr += (ext->ip6e_len + 1) * 8;
|
||||
ptr += (static_cast<std::size_t>(ext->ip6e_len) + 1U) * 8U;
|
||||
break;
|
||||
}
|
||||
case IPPROTO_FRAGMENT: {
|
||||
|
|
@ -136,18 +139,19 @@ IPv6::IPv6(const u_char *data) {
|
|||
}
|
||||
|
||||
void IPv6::handle_tcp() {
|
||||
const auto tcp = reinterpret_cast<const tcphdr *>(ptr);
|
||||
const auto *const tcp = reinterpret_cast<const tcphdr *>(ptr);
|
||||
dest_port = ntohs(tcp->dest);
|
||||
src_port = ntohs(tcp->source);
|
||||
|
||||
payload_ptr = reinterpret_cast<const uint8_t *>(tcp) + tcp->doff * 4;
|
||||
payload_len = ntohs(ip_hdr->ip6_plen) - tcp->doff * 4;
|
||||
const auto tcp_hdr_len = static_cast<std::size_t>(tcp->doff) * 4U;
|
||||
payload_ptr = reinterpret_cast<const uint8_t *>(tcp) + tcp_hdr_len;
|
||||
payload_len = static_cast<uint16_t>(ntohs(ip_hdr->ip6_plen) - static_cast<uint16_t>(tcp_hdr_len));
|
||||
|
||||
protocol = TransportProtocol::TCP;
|
||||
ptr = nullptr;
|
||||
}
|
||||
void IPv6::handle_udp() {
|
||||
const auto udp = reinterpret_cast<const udphdr *>(ptr);
|
||||
const auto *const udp = reinterpret_cast<const udphdr *>(ptr);
|
||||
dest_port = ntohs(udp->dest);
|
||||
src_port = ntohs(udp->source);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,25 +2,28 @@
|
|||
#include <cstring>
|
||||
|
||||
ApplicationProtocol Packet::get_application_protocol() {
|
||||
if (!payload_ptr || payload_len < 4)
|
||||
goto check_port;
|
||||
if (payload_ptr != nullptr && payload_len >= 4) {
|
||||
if (transport_protocol == TransportProtocol::TCP) {
|
||||
if (memcmp(payload_ptr, "GET ", 4) == 0 || memcmp(payload_ptr, "POST", 4) == 0 ||
|
||||
memcmp(payload_ptr, "HEAD", 4) == 0 || memcmp(payload_ptr, "PUT ", 4) == 0 ||
|
||||
memcmp(payload_ptr, "HTTP", 4) == 0) {
|
||||
return ApplicationProtocol::HTTP;
|
||||
}
|
||||
}
|
||||
|
||||
if (transport_protocol == TransportProtocol::TCP) {
|
||||
if (!memcmp(payload_ptr, "GET ", 4) || !memcmp(payload_ptr, "POST", 4) || !memcmp(payload_ptr, "HEAD", 4) ||
|
||||
!memcmp(payload_ptr, "PUT ", 4) || !memcmp(payload_ptr, "HTTP", 4))
|
||||
return ApplicationProtocol::HTTP;
|
||||
if ((src_port == 53 || dst_port == 53) && payload_len >= 12) {
|
||||
return ApplicationProtocol::DNS;
|
||||
}
|
||||
|
||||
if (transport_protocol == TransportProtocol::TCP && payload_len >= 3) {
|
||||
if (payload_ptr[0] == 0x16 && payload_ptr[1] == 0x03) {
|
||||
return ApplicationProtocol::HTTPS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((src_port == 53 || dst_port == 53) && payload_len >= 12) {
|
||||
return ApplicationProtocol::DNS;
|
||||
}
|
||||
if (transport_protocol == TransportProtocol::TCP && payload_len >= 3) {
|
||||
if (payload_ptr[0] == 0x16 && payload_ptr[1] == 0x03)
|
||||
return ApplicationProtocol::HTTPS;
|
||||
}
|
||||
const uint16_t port = (src_port < dst_port) ? src_port : dst_port;
|
||||
|
||||
check_port:
|
||||
uint16_t port = (src_port < dst_port) ? src_port : dst_port;
|
||||
if (transport_protocol == TransportProtocol::TCP) {
|
||||
switch (port) {
|
||||
case 21:
|
||||
|
|
|
|||
|
|
@ -203,20 +203,6 @@ void Stats::update_packets() {
|
|||
}
|
||||
}
|
||||
|
||||
double Stats::smooth_value(size_t i, size_t start) {
|
||||
const int window = 3;
|
||||
double sum = 0.0;
|
||||
int count = 0;
|
||||
|
||||
for (int k = -window; k <= window; ++k) {
|
||||
long idx = (long)i + k;
|
||||
if (idx >= (long)start && idx < (long)snapshot.bandwidth_history.size()) {
|
||||
sum += snapshot.bandwidth_history[idx].bytes_per_sec;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count ? sum / count : 0.0;
|
||||
}
|
||||
/**
|
||||
* @brief Calculates current bandwidth (bytes/sec).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -22,4 +22,3 @@ dmypy.json
|
|||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
|
|||
| **[DNS Lookup CLI Tool](./PROJECTS/beginner/dns-lookup)**<br>Query DNS records with WHOIS |    | DNS protocols • WHOIS queries • Reverse DNS lookup<br>[Source Code](./PROJECTS/beginner/dns-lookup) \| [Docs](./PROJECTS/beginner/dns-lookup/learn) |
|
||||
| **[Simple Vulnerability Scanner](./PROJECTS/beginner/simple-vulnerability-scanner)**<br>Check software against CVE databases |    | CVE databases • Dependency scanning • Vulnerability assessment<br>[Source Code](./PROJECTS/beginner/simple-vulnerability-scanner) \| [Docs](./PROJECTS/beginner/simple-vulnerability-scanner/learn) |
|
||||
| **[Metadata Scrubber Tool](./PROJECTS/beginner/metadata-scrubber-tool)**<br>Remove EXIF and privacy metadata [@Heritage-XioN](https://github.com/Heritage-XioN) |    | EXIF data • Privacy protection • Batch processing<br>[Source Code](./PROJECTS/beginner/metadata-scrubber-tool) \| [Docs](./PROJECTS/beginner/metadata-scrubber-tool/learn) |
|
||||
| **[Network Traffic Analyzer](./PROJECTS/beginner/network-traffic-analyzer)**<br>Capture and analyze packets |    | Packet capture • Protocol analysis • Traffic visualization<br>[Source Code](./PROJECTS/beginner/network-traffic-analyzer) \| [Docs](./PROJECTS/beginner/network-traffic-analyzer/learn) |
|
||||
| **[Network Traffic Analyzer](./PROJECTS/beginner/network-traffic-analyzer)**<br>Capture and analyze packets |     | Packet capture • Protocol analysis • Traffic visualization<br>[Source (C++)](./PROJECTS/beginner/network-traffic-analyzer/cpp) \| [Docs (C++)](./PROJECTS/beginner/network-traffic-analyzer/cpp/learn) \| [Source (Python)](./PROJECTS/beginner/network-traffic-analyzer/python) \| [Docs (Python)](./PROJECTS/beginner/network-traffic-analyzer/python/learn) |
|
||||
| **[Hash Cracker](./SYNOPSES/beginner/Hash.Cracker.md)**<br>Dictionary and brute-force cracking |    | Hash algorithms • Dictionary attacks • Password security<br>[Learn More](./SYNOPSES/beginner/Hash.Cracker.md) |
|
||||
| **[Steganography Tool](./SYNOPSES/beginner/Steganography.Tool.md)**<br>Hide messages in images |    | LSB steganography • Image manipulation • Data hiding<br>[Learn More](./SYNOPSES/beginner/Steganography.Tool.md) |
|
||||
| **[MAC Address Spoofer](./SYNOPSES/beginner/MAC.Address.Spoofer.md)**<br>Change network interface MAC |    | Network interfaces • MAC addresses • Vendor lookup<br>[Learn More](./SYNOPSES/beginner/MAC.Address.Spoofer.md) |
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0eec274fba59ddd373080ae961dd4497eaf2e454
|
||||
Subproject commit ecbb534e85e8e381e6e89aece4b786db8f7ad172
|
||||
Loading…
Reference in New Issue