Compare commits
21 Commits
d590d653f9
...
449b1a5ac6
| Author | SHA1 | Date | |
|---|---|---|---|
| 449b1a5ac6 | |||
| 84a1c29e1e | |||
| 67dcae2e40 | |||
| fbaef94082 | |||
| 12e0ff70cc | |||
|
|
d47d08d79e | ||
|
|
ebc7edcead | ||
|
|
18a2d4a168 | ||
| 26f2e1851b | |||
| 55edf01113 | |||
| f2c4be77a6 | |||
|
|
32d93906f2 | ||
|
|
d93b43f463 | ||
|
|
97d511d17a | ||
|
|
24af672c41 | ||
|
|
5987f2793d | ||
|
|
c4598970a2 | ||
|
|
688314da66 | ||
|
|
3c912bb2d4 | ||
|
|
d01045e74a | ||
|
|
f021c8ebe2 |
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
BIN
.github/images/architecture.png
vendored
Normal file
|
After Width: | Height: | Size: 163 KiB |
105
.github/images/architecture.svg
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 900" width="1200" height="900">
|
||||
<defs>
|
||||
<style>
|
||||
text { font-family: "Inter", "Segoe UI", "Noto Sans", sans-serif; fill: #e5e7eb; }
|
||||
.title { font-size: 26px; font-weight: 700; fill: #f9fafb; }
|
||||
.band-label { font-size: 11px; font-weight: 600; letter-spacing: 1.5px; fill: #9ca3af; }
|
||||
.box-title { font-size: 14px; font-weight: 600; fill: #f3f4f6; }
|
||||
.box-title-lg { font-size: 16px; font-weight: 700; fill: #f3f4f6; }
|
||||
.box-sub { font-size: 12px; fill: #cbd5e1; }
|
||||
.box-bullet { font-size: 12px; fill: #cbd5e1; }
|
||||
.arrow-label { font-size: 11px; font-weight: 500; fill: #d1d5db; }
|
||||
.arrow-label-bg { fill: #0f0f1a; }
|
||||
.legend-label { font-size: 11px; fill: #9ca3af; }
|
||||
</style>
|
||||
<marker id="arrow-blue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#60a5fa"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#34d399"/>
|
||||
</marker>
|
||||
<marker id="arrow-orange" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#fb923c"/>
|
||||
</marker>
|
||||
<marker id="arrow-gray" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#9ca3af"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#a78bfa"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="1200" height="900" fill="#0f0f1a"/>
|
||||
<text class="title" x="600" y="46" text-anchor="middle">NetGuardia architecture</text>
|
||||
<rect x="40" y="70" width="1120" height="80" rx="10" fill="#161627" stroke="#2a2a4a" stroke-width="1"/>
|
||||
<text class="band-label" x="60" y="92">CONTROL PLANE</text>
|
||||
<rect x="400" y="100" width="400" height="40" rx="8" fill="#1f1f36" stroke="#60a5fa" stroke-width="1.5"/>
|
||||
<text class="box-title" x="600" y="126" text-anchor="middle">Web UI + REST / WebSocket API</text>
|
||||
<rect x="40" y="170" width="1120" height="500" rx="10" fill="#161627" stroke="#2a2a4a" stroke-width="1"/>
|
||||
<text class="band-label" x="60" y="192">USER SPACE — DETECTION & RESPONSE</text>
|
||||
<rect x="115" y="210" width="220" height="80" rx="8" fill="#1f1f36" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<text class="box-title" x="225" y="244" text-anchor="middle">ML Inference</text>
|
||||
<text class="box-sub" x="225" y="266" text-anchor="middle">tract-onnx · BYO model</text>
|
||||
<rect x="365" y="210" width="220" height="80" rx="8" fill="#1f1f36" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<text class="box-title" x="475" y="244" text-anchor="middle">Beaconing</text>
|
||||
<text class="box-sub" x="475" y="266" text-anchor="middle">temporal CV</text>
|
||||
<rect x="615" y="210" width="220" height="80" rx="8" fill="#1f1f36" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<text class="box-title" x="725" y="244" text-anchor="middle">Correlation</text>
|
||||
<text class="box-sub" x="725" y="266" text-anchor="middle">graph topology</text>
|
||||
<rect x="865" y="210" width="220" height="80" rx="8" fill="#1f1f36" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<text class="box-title" x="975" y="244" text-anchor="middle">Suricata</text>
|
||||
<text class="box-sub" x="975" y="266" text-anchor="middle">eve.json ingest</text>
|
||||
<rect x="300" y="350" width="600" height="130" rx="10" fill="#1f1f36" stroke="#60a5fa" stroke-width="2"/>
|
||||
<text class="box-title-lg" x="600" y="378" text-anchor="middle">Detection Orchestrator</text>
|
||||
<text class="box-bullet" x="340" y="410">• canonicalize attack type</text>
|
||||
<text class="box-bullet" x="340" y="432">• fuse: 1 − ∏(1 − c_i)</text>
|
||||
<text class="box-bullet" x="340" y="454">• WORM audit (SHA-256 chained)</text>
|
||||
<rect x="300" y="520" width="600" height="130" rx="10" fill="#1f1f36" stroke="#fb923c" stroke-width="2"/>
|
||||
<text class="box-title-lg" x="600" y="548" text-anchor="middle">SOAR Engine</text>
|
||||
<text class="box-bullet" x="340" y="580">• playbook match</text>
|
||||
<text class="box-bullet" x="340" y="602">• cooldown + dry-run</text>
|
||||
<text class="box-bullet" x="340" y="624">• actions: block / rate-limit / webhook / email / telegram</text>
|
||||
<rect x="40" y="690" width="1120" height="140" rx="10" fill="#161627" stroke="#2a2a4a" stroke-width="1"/>
|
||||
<text class="band-label" x="60" y="712">KERNEL SPACE — eBPF / XDP</text>
|
||||
<rect x="80" y="750" width="180" height="60" rx="8" fill="#1f1f36" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text class="box-title" x="170" y="787" text-anchor="middle">ACL</text>
|
||||
<rect x="295" y="750" width="180" height="60" rx="8" fill="#1f1f36" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text class="box-title" x="385" y="787" text-anchor="middle">Rate Limit</text>
|
||||
<rect x="510" y="750" width="180" height="60" rx="8" fill="#1f1f36" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text class="box-title" x="600" y="787" text-anchor="middle">Protocol Filter</text>
|
||||
<rect x="725" y="750" width="180" height="60" rx="8" fill="#1f1f36" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text class="box-title" x="815" y="787" text-anchor="middle">Geo Block</text>
|
||||
<rect x="940" y="750" width="180" height="60" rx="8" fill="#1f1f36" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text class="box-title" x="1030" y="787" text-anchor="middle">DNS Filter</text>
|
||||
<line x1="262" y1="780" x2="293" y2="780" stroke="#34d399" stroke-width="1.8" marker-end="url(#arrow-green)"/>
|
||||
<line x1="477" y1="780" x2="508" y2="780" stroke="#34d399" stroke-width="1.8" marker-end="url(#arrow-green)"/>
|
||||
<line x1="692" y1="780" x2="723" y2="780" stroke="#34d399" stroke-width="1.8" marker-end="url(#arrow-green)"/>
|
||||
<line x1="907" y1="780" x2="938" y2="780" stroke="#34d399" stroke-width="1.8" marker-end="url(#arrow-green)"/>
|
||||
<line x1="590" y1="140" x2="590" y2="348" stroke="#9ca3af" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow-gray)"/>
|
||||
<line x1="610" y1="348" x2="610" y2="142" stroke="#9ca3af" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow-gray)"/>
|
||||
<rect class="arrow-label-bg" x="535" y="156" width="130" height="18" rx="2"/>
|
||||
<text class="arrow-label" x="600" y="169" text-anchor="middle">admin & live events</text>
|
||||
<path d="M 225,290 L 225,320 L 420,340 L 420,350" fill="none" stroke="#60a5fa" stroke-width="1.8" marker-end="url(#arrow-blue)"/>
|
||||
<path d="M 475,290 L 475,320 L 540,340 L 540,350" fill="none" stroke="#60a5fa" stroke-width="1.8" marker-end="url(#arrow-blue)"/>
|
||||
<path d="M 725,290 L 725,320 L 660,340 L 660,350" fill="none" stroke="#60a5fa" stroke-width="1.8" marker-end="url(#arrow-blue)"/>
|
||||
<path d="M 975,290 L 975,320 L 780,340 L 780,350" fill="none" stroke="#60a5fa" stroke-width="1.8" marker-end="url(#arrow-blue)"/>
|
||||
<line x1="600" y1="480" x2="600" y2="520" stroke="#34d399" stroke-width="2" marker-end="url(#arrow-green)"/>
|
||||
<rect class="arrow-label-bg" x="612" y="488" width="135" height="18" rx="2"/>
|
||||
<text class="arrow-label" x="680" y="501" text-anchor="middle">ThreatDetectedEvent</text>
|
||||
<path d="M 320,650 L 320,680 L 115,680 L 115,745 L 170,745 L 170,750" fill="none" stroke="#fb923c" stroke-width="2" marker-end="url(#arrow-orange)"/>
|
||||
<rect class="arrow-label-bg" x="130" y="670" width="170" height="18" rx="2"/>
|
||||
<text class="arrow-label" x="215" y="683" text-anchor="middle">enforce (block / rate-limit)</text>
|
||||
<path d="M 1095,750 L 1095,310 L 1005,310 L 1005,290" fill="none" stroke="#a78bfa" stroke-width="2" marker-end="url(#arrow-purple)"/>
|
||||
<rect class="arrow-label-bg" x="1030" y="468" width="100" height="18" rx="2"/>
|
||||
<text class="arrow-label" x="1080" y="481" text-anchor="middle">AF_XDP mirror</text>
|
||||
<line x1="60" y1="870" x2="84" y2="870" stroke="#60a5fa" stroke-width="2"/>
|
||||
<text class="legend-label" x="90" y="874">detection events</text>
|
||||
<line x1="216" y1="870" x2="240" y2="870" stroke="#34d399" stroke-width="2"/>
|
||||
<text class="legend-label" x="246" y="874">fused event / kernel pipeline</text>
|
||||
<line x1="450" y1="870" x2="474" y2="870" stroke="#fb923c" stroke-width="2"/>
|
||||
<text class="legend-label" x="480" y="874">enforcement action</text>
|
||||
<line x1="618" y1="870" x2="642" y2="870" stroke="#a78bfa" stroke-width="2"/>
|
||||
<text class="legend-label" x="648" y="874">AF_XDP mirror</text>
|
||||
<line x1="756" y1="870" x2="780" y2="870" stroke="#9ca3af" stroke-width="2"/>
|
||||
<text class="legend-label" x="786" y="874">admin / control</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
BIN
.github/images/ui/access-control.png
vendored
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
.github/images/ui/account-management.png
vendored
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
.github/images/ui/api-keys.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
.github/images/ui/audit-log.png
vendored
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
.github/images/ui/auto-response.png
vendored
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
.github/images/ui/detection.png
vendored
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
.github/images/ui/dns-filter.png
vendored
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
.github/images/ui/drop-monitor.png
vendored
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
.github/images/ui/flow-trace.png
vendored
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
.github/images/ui/geoip-block.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
.github/images/ui/logs.png
vendored
Normal file
|
After Width: | Height: | Size: 267 KiB |
BIN
.github/images/ui/map.png
vendored
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
.github/images/ui/protocol-filter.png
vendored
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
.github/images/ui/rate-limit.png
vendored
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
.github/images/ui/security-report.png
vendored
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
.github/images/ui/statistics.png
vendored
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
.github/images/ui/system-settings.png
vendored
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
.github/images/ui/system-status.png
vendored
Normal file
|
After Width: | Height: | Size: 151 KiB |
119
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,119 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dalaw2-dev]
|
||||
pull_request:
|
||||
branches: [master, dalaw2-dev]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve frontend submodule commit
|
||||
id: frontend-ref
|
||||
run: echo "sha=$(git rev-parse HEAD:net-guardia-frontend)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout frontend submodule
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: DaLaw2/NetGuardia-FrontEnd
|
||||
ref: ${{ steps.frontend-ref.outputs.sha }}
|
||||
path: net-guardia-frontend
|
||||
token: ${{ secrets.SUBMODULE_PAT }}
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
gcc m4 clang llvm \
|
||||
libelf-dev zlib1g-dev pkg-config
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: net-guardia-frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
working-directory: net-guardia-frontend
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Install Rust nightly toolchain (for eBPF)
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rust-src
|
||||
|
||||
# Ensure stable is the default so cargo check/test/clippy use stable.
|
||||
# Nightly is only needed for the eBPF subprocess (which uses
|
||||
# rust-toolchain.toml in ingress-ebpf/egress-ebpf).
|
||||
- name: Set stable as default toolchain
|
||||
run: rustup default stable
|
||||
|
||||
- name: Cache cargo registry and build artifacts
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install bpf-linker
|
||||
run: |
|
||||
cargo install cargo-binstall --locked 2>/dev/null || true
|
||||
if command -v cargo-binstall &>/dev/null; then
|
||||
cargo binstall bpf-linker --no-confirm --force || cargo install bpf-linker --locked
|
||||
else
|
||||
cargo install bpf-linker --locked
|
||||
fi
|
||||
ls -la "$HOME/.cargo/bin/bpf-linker"
|
||||
timeout-minutes: 45
|
||||
|
||||
- name: cargo check default workspace members
|
||||
run: cargo check
|
||||
|
||||
- name: cargo test default workspace members
|
||||
run: cargo test
|
||||
|
||||
- name: cargo clippy default workspace members
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
- name: Frontend build
|
||||
run: npm run build
|
||||
working-directory: net-guardia-frontend
|
||||
|
||||
- name: Frontend tests
|
||||
run: npm test
|
||||
working-directory: net-guardia-frontend
|
||||
|
||||
integration-test:
|
||||
name: Integration Test (placeholder)
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-test
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Integration test reminder
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Integration tests are not run in CI."
|
||||
echo " They require the podman test environment."
|
||||
echo ""
|
||||
echo " Run on the dev server:"
|
||||
echo " bash /home/dalaw2/test_netguardia.sh"
|
||||
echo "============================================"
|
||||
40
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
# Claude Code
|
||||
.claude/
|
||||
|
||||
### https://raw.github.com/github/gitignore/master/Rust.gitignore
|
||||
|
||||
# Generated by Cargo
|
||||
@ -10,3 +13,40 @@ target/
|
||||
|
||||
.idea
|
||||
logs
|
||||
TODO
|
||||
.log
|
||||
.txt
|
||||
.csv
|
||||
net-guardia/static/web
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Profiling
|
||||
*.profraw
|
||||
*.profdata
|
||||
|
||||
*.hex
|
||||
.gstack/
|
||||
interfaces.txt
|
||||
traffic_log.csv
|
||||
|
||||
CLAUDE.md
|
||||
AGENT.md
|
||||
DESIGN.md
|
||||
TODOS.md
|
||||
VERSION
|
||||
CHANGELOG.md
|
||||
|
||||
# Benchmark data/results (local only)
|
||||
benchmark/
|
||||
docs/
|
||||
|
||||
# SQLite database files
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
7
.gitmodules
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
[submodule "net-guardia-frontend"]
|
||||
path = net-guardia-frontend
|
||||
url = https://github.com/DaLaw2/NetGuardia-FrontEnd.git
|
||||
[submodule "net-guardia-trainer"]
|
||||
path = net-guardia-trainer
|
||||
url = https://github.com/ParrotXray/NetGuardia-Trainer.git
|
||||
branch = dalaw2-dev
|
||||
4303
Cargo.lock
generated
125
Cargo.toml
@ -1,41 +1,114 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["net-guardia", "net-guardia-common", "net-guardia-ingress-ebpf", "net-guardia-egress-ebpf"]
|
||||
default-members = ["net-guardia", "net-guardia-common"]
|
||||
members = ["net-guardia", "net-guardia-abi", "macros", "ingress-ebpf", "egress-ebpf", "net-guardia-cli"]
|
||||
default-members = ["net-guardia", "net-guardia-abi", "net-guardia-cli"]
|
||||
|
||||
[workspace.dependencies]
|
||||
aya = { version = "0.13.0", default-features = false }
|
||||
aya-ebpf = { version = "0.1.1", default-features = false }
|
||||
aya-log = { version = "0.2.1", default-features = false }
|
||||
aya-log-ebpf = { version = "0.1.1", default-features = false }
|
||||
# Local crates
|
||||
net-guardia-abi = { path = "net-guardia-abi" }
|
||||
macros = { path = "macros" }
|
||||
|
||||
anyhow = { version = "1", default-features = false }
|
||||
cargo_metadata = { version = "0.19.0", default-features = false }
|
||||
# `std` feature is currently required to build `clap`.
|
||||
#
|
||||
# See https://github.com/clap-rs/clap/blob/61f5ee5/clap_builder/src/lib.rs#L15.
|
||||
clap = { version = "4.5.20", default-features = false, features = ["std"] }
|
||||
env_logger = { version = "0.11.5", default-features = false }
|
||||
libc = { version = "0.2.159", default-features = false }
|
||||
log = { version = "0.4.22", default-features = false }
|
||||
tokio = { version = "1.40.0", default-features = false }
|
||||
which = { version = "7.0.0", default-features = false }
|
||||
# eBPF - kernel side (pinned: aya-ebpf 0.1.2 was yanked, see aya-rs/aya#1400)
|
||||
aya-ebpf = { version = "=0.1.1", default-features = false }
|
||||
aya-log-ebpf = { version = "=0.1.0", default-features = false }
|
||||
|
||||
# eBPF - userspace side
|
||||
aya = { version = "0.13.1", default-features = false }
|
||||
aya-log = { version = "0.2.1", default-features = false }
|
||||
network-types = { version = "0.2.0", default-features = false }
|
||||
|
||||
# XDP
|
||||
xsk-rs = { version = "0.8.0", default-features = false }
|
||||
libxdp-sys = { version = "0.2.4", features = ["use_cc_build", "use_precompiled_bpf"] }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "sync", "time", "process", "io-util", "io-std", "fs", "signal"] }
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
|
||||
# Web framework
|
||||
actix = "0.13.5"
|
||||
actix-web = "4.13.0"
|
||||
actix-cors = "0.7.1"
|
||||
actix-ws = "0.4.0"
|
||||
actix-multipart = "0.7"
|
||||
actix-files = "0.6"
|
||||
tokio-tungstenite = "0.29.0"
|
||||
zip = "8.6.0"
|
||||
|
||||
# Logging / tracing
|
||||
tracing = "0.1.44"
|
||||
tracing-appender = "0.2.4"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "registry"] }
|
||||
|
||||
# ML
|
||||
tract-onnx = "0.22.1"
|
||||
|
||||
# Utilities
|
||||
parking_lot = "0.12.5"
|
||||
libc = { version = "0.2.183", default-features = false }
|
||||
thiserror = "2.0.18"
|
||||
futures-util = "0.3.32"
|
||||
crossbeam = "0.8.4"
|
||||
|
||||
# System
|
||||
sysinfo = "0.39.0"
|
||||
maxminddb = "0.28.1"
|
||||
ipnetwork = "0.21.1"
|
||||
lru = "0.18.0"
|
||||
rusqlite = { version = "0.39", features = ["bundled-sqlcipher"] }
|
||||
async-sqlite = { version = "0.5.7", default-features = false, features = ["bundled-sqlcipher"] }
|
||||
argon2 = "0.5"
|
||||
rand = "0.10.1"
|
||||
ed25519-dalek = { version = "2", features = ["std", "rand_core"] }
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
rust-embed = "8.11.0"
|
||||
mime_guess = "2.0.5"
|
||||
url = "2.5.8"
|
||||
toml = "1.0.7"
|
||||
lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "smtp-transport", "tokio1-rustls-tls"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
reqwest = { version = "0.13.3", default-features = false, features = ["json", "rustls"] }
|
||||
async-trait = "0.1"
|
||||
dashmap = "6"
|
||||
arc-swap = "1"
|
||||
moka = { version = "0.12", features = ["sync"] }
|
||||
notify = "8.2.0"
|
||||
sha2 = "0.11.0"
|
||||
hmac = "0.13.0"
|
||||
aes-gcm = "0.10"
|
||||
hkdf = "0.13.0"
|
||||
sd-notify = "0.5.0"
|
||||
|
||||
# Build dependencies
|
||||
cargo_metadata = { version = "0.23.1", default-features = false }
|
||||
which = "8.0.2"
|
||||
dotenvy = "0.15.7"
|
||||
|
||||
# Proc macro
|
||||
proc-macro2 = "1.0.106"
|
||||
quote = "1.0.45"
|
||||
syn = { version = "2.0.117", features = ["full"] }
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
panic = "unwind"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
#opt-level = 3
|
||||
#lto = true
|
||||
#strip = true
|
||||
#debug = false
|
||||
#overflow-checks = false
|
||||
panic = "unwind"
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
strip = true
|
||||
|
||||
[profile.release.package.net-guardia-ingress-ebpf]
|
||||
[profile.release.package.ingress-ebpf]
|
||||
debug = 2
|
||||
codegen-units = 1
|
||||
|
||||
[profile.release.package.net-guardia-egress-ebpf]
|
||||
[profile.release.package.egress-ebpf]
|
||||
debug = 2
|
||||
codegen-units = 1
|
||||
|
||||
95
README.md
@ -1,45 +1,76 @@
|
||||
# NetGuardia 🛡️
|
||||
# NetGuardia
|
||||
|
||||
## 📌 專案簡介
|
||||
Inline network security platform built on eBPF/XDP. Combines ONNX-based ML, temporal beaconing, correlation heuristics, and Suricata `eve.json` alerts in one fusion path, drives SOAR playbooks, and writes decisions into a WORM audit chain.
|
||||
|
||||
**NetGuardia** 是一個結合 eBPF XDP 與深度學習模型的實體網路防護裝置,運行於 Raspberry Pi 5 與 Intel i350 T2 網卡。本專案旨在提供高效能、低延遲的網路防護解決方案,適用於家庭與中小型企業環境。
|
||||
## Stack
|
||||
|
||||
## 🔧 核心技術
|
||||
- **Data plane** — eBPF / XDP / AF_XDP (aya, xsk-rs)
|
||||
- **Detection** — Rust + tract-onnx for ML, custom temporal / graph engines, Suricata `eve.json` ingest
|
||||
- **Control plane** — actix-web REST + WebSocket, SQLite + SQLCipher, argon2 / JWT / CSRF, per-playbook SOAR
|
||||
- **Frontend** — Vue 3 + Pinia + Vue-i18n (en / zh-TW / zh-CN / ja)
|
||||
- **Architecture** — hexagonal-ish Rust workspace: `domain/` · `interface/` · `core/` · `adapter/` · `infrastructure/`
|
||||
|
||||
- **eBPF XDP 技術** - 實現高效能網路封包處理,直接在資料連結層操作
|
||||
- **深度學習模型** - 識別與預測潛在網路攻擊,提供智慧化防護
|
||||
- **嵌入式硬體整合** - 結合 Raspberry Pi 5 與 Intel i350 T2,提供獨立且強大的網路防護功能
|
||||
## Screens
|
||||
|
||||
## 🧩 功能模組
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src=".github/images/ui/statistics.png" alt="Traffic statistics"/><br><sub>Traffic statistics (per-IP bytes/packets)</sub></td>
|
||||
<td><img src=".github/images/ui/map.png" alt="Geo map"/><br><sub>Live geographic flow map</sub></td>
|
||||
<td><img src=".github/images/ui/drop-monitor.png" alt="Drop monitor"/><br><sub>Real-time drop monitor</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src=".github/images/ui/detection.png" alt="Threat detection"/><br><sub>Fused threat detection + ML status</sub></td>
|
||||
<td><img src=".github/images/ui/access-control.png" alt="Access control"/><br><sub>IPv4/IPv6 allow + block lists</sub></td>
|
||||
<td><img src=".github/images/ui/geoip-block.png" alt="GeoIP block"/><br><sub>GeoIP country block</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src=".github/images/ui/dns-filter.png" alt="DNS filter"/><br><sub>DNS blacklist</sub></td>
|
||||
<td><img src=".github/images/ui/rate-limit.png" alt="Rate limit"/><br><sub>Per-class DDoS rate limits</sub></td>
|
||||
<td><img src=".github/images/ui/protocol-filter.png" alt="Protocol filter"/><br><sub>HTTP / SSH service rules</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src=".github/images/ui/auto-response.png" alt="SOAR"/><br><sub>SOAR playbooks + dry-run</sub></td>
|
||||
<td><img src=".github/images/ui/security-report.png" alt="Security report"/><br><sub>Security report (PDF / email)</sub></td>
|
||||
<td><img src=".github/images/ui/audit-log.png" alt="Audit log"/><br><sub>WORM-chained audit log</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src=".github/images/ui/account-management.png" alt="Accounts"/><br><sub>Users + groups + RBAC</sub></td>
|
||||
<td><img src=".github/images/ui/api-keys.png" alt="API keys"/><br><sub>API keys</sub></td>
|
||||
<td><img src=".github/images/ui/flow-trace.png" alt="Flow trace"/><br><sub>Rotated flow recording</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src=".github/images/ui/logs.png" alt="Logs"/><br><sub>Live + archived logs</sub></td>
|
||||
<td><img src=".github/images/ui/system-status.png" alt="System status"/><br><sub>CPU / memory / NIC counters</sub></td>
|
||||
<td><img src=".github/images/ui/system-settings.png" alt="System settings"/><br><sub>Mode / theme / HTTP / engine</sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 📊 儀表板總覽 (Dashboard)
|
||||

|
||||
- 即時網路流量監控與視覺化統計
|
||||
- 近期流量大小統計與趨勢圖
|
||||
## Architecture
|
||||
|
||||
### 📈 詳細流量統計 (Statistics)
|
||||

|
||||
- 各 IP 位址詳細流量使用情況
|
||||

|
||||
|
||||
### 🔒 網路存取控制 (Access Control)
|
||||

|
||||
- IPv4/IPv6 黑白名單管理
|
||||
- 精確的連接埠層級存取控制
|
||||
## Requirements
|
||||
|
||||
### 🤖 AI 攻擊偵測 (AI Detection)
|
||||

|
||||
- 基於 AI 的攻擊偵測引擎
|
||||
Linux kernel with eBPF **and** a NIC driver that implements AF_XDP on that kernel. No single "minimum kernel" — it depends on the NIC.
|
||||
|
||||
## ✨ 系統特色
|
||||
| Driver | NIC family | Min kernel for AF_XDP |
|
||||
|---|---|---|
|
||||
| `mlx5` | Mellanox ConnectX-4/5/6/7 | 5.x |
|
||||
| `ixgbe` | Intel 82599, X520, X540, X550 | 5.x |
|
||||
| `i40e` | Intel X710, XL710, XXV710 | 5.x |
|
||||
| `ice` | Intel E810 | 5.5+ |
|
||||
| `igb` | Intel i350 T2 (reference HW) | **6.17** |
|
||||
| `igc` | Intel I225/I226 | 6.x |
|
||||
| `virtio_net` | QEMU/KVM | varies |
|
||||
|
||||
- **⚡ 高效能** - 低延遲封包處理,最小化網路效能影響
|
||||
- **👥 易用性** - 跨平台 Web 管理介面,直覺操作
|
||||
- **🔄 可靠性** - 硬體加速處理,確保穩定運行
|
||||
- **📦 可擴展** - 模組化設計,支援功能擴展
|
||||
Check with `ethtool -i <iface>` before deploying. 8 GB RAM minimum, 16 GB+ for high-traffic.
|
||||
|
||||
## 💻 安裝需求
|
||||
## Build
|
||||
|
||||
- Raspberry Pi 5 (建議 8GB RAM 版本)
|
||||
- 雙孔網卡(能支援 XDP native/offload 更好)
|
||||
- 32GB 以上 microSD 卡 (建議 Class 10 以上)
|
||||
- 5V/3A 以上電源供應器
|
||||
```sh
|
||||
cargo build --release --package net-guardia
|
||||
sudo ./target/release/net-guardia
|
||||
# open http://<host>:8080 — setup wizard issues the admin password on first boot
|
||||
```
|
||||
|
||||
Systemd unit: [`deploy/netguardia.service`](deploy/netguardia.service).
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
[Config]
|
||||
ingress_ifindex = "enp1s0f0" # nic name
|
||||
egress_ifindex = "enp1s0f1" # nic name
|
||||
management_ifindex = "wlan0" # nic name
|
||||
alert_path = "/tmp/alert"
|
||||
http_server_bind_port = 8080 # port
|
||||
refresh_interval = 1 # seconds
|
||||
25
deploy/compose/Containerfile.endpoint
Normal file
@ -0,0 +1,25 @@
|
||||
FROM rockylinux:10
|
||||
|
||||
RUN dnf install -y epel-release && \
|
||||
dnf install -y --allowerasing \
|
||||
nmap \
|
||||
tcpdump \
|
||||
net-tools \
|
||||
iputils \
|
||||
iproute \
|
||||
ncat \
|
||||
curl wget \
|
||||
httpd \
|
||||
openssh-server \
|
||||
python3 python3-pip \
|
||||
&& dnf clean all
|
||||
|
||||
RUN pip3 install scapy requests
|
||||
|
||||
RUN echo "NetGuardia Endpoint" > /var/www/html/index.html && \
|
||||
ssh-keygen -A
|
||||
|
||||
COPY scripts/*.sh /opt/scripts/
|
||||
RUN chmod +x /opt/scripts/*.sh
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
41
deploy/compose/Containerfile.netguardia
Normal file
@ -0,0 +1,41 @@
|
||||
FROM rockylinux:10
|
||||
|
||||
RUN dnf install -y epel-release && \
|
||||
crb enable && \
|
||||
dnf install -y --allowerasing \
|
||||
clang llvm \
|
||||
bpftool \
|
||||
iproute iproute-tc \
|
||||
elfutils-libelf-devel \
|
||||
zlib-devel \
|
||||
libbpf-devel \
|
||||
kernel-headers \
|
||||
tcpdump \
|
||||
net-tools \
|
||||
iputils \
|
||||
curl wget \
|
||||
git \
|
||||
gh \
|
||||
vim \
|
||||
ethtool \
|
||||
nodejs24 \
|
||||
nodejs24-npm \
|
||||
m4 \
|
||||
make pkg-config \
|
||||
openssl-devel \
|
||||
&& dnf clean all
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.95.0 \
|
||||
&& /root/.cargo/bin/rustup component add clippy rustfmt --toolchain 1.95.0 \
|
||||
&& /root/.cargo/bin/rustup toolchain install nightly --component rust-src \
|
||||
&& /root/.cargo/bin/rustup default 1.95.0
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN cargo install bpf-linker --version 0.10.3 --locked
|
||||
|
||||
RUN ln -s /usr/bin/node-24 /usr/local/bin/node && \
|
||||
ln -s /usr/bin/npm-24 /usr/local/bin/npm && \
|
||||
ln -s /usr/bin/npx-24 /usr/local/bin/npx
|
||||
|
||||
WORKDIR /root/NetGuardia
|
||||
CMD ["sleep", "infinity"]
|
||||
10
deploy/compose/Containerfile.router
Normal file
@ -0,0 +1,10 @@
|
||||
FROM rockylinux:10
|
||||
|
||||
RUN dnf install -y --allowerasing \
|
||||
iproute \
|
||||
iputils \
|
||||
net-tools \
|
||||
tcpdump \
|
||||
&& dnf clean all
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
78
deploy/compose/podman-compose.yml
Normal file
@ -0,0 +1,78 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
netguardia:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: compose/Containerfile.netguardia
|
||||
image: netguardia:latest
|
||||
container_name: netguardia
|
||||
hostname: netguardia
|
||||
privileged: true
|
||||
security_opt:
|
||||
- seccomp=unconfined
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
environment:
|
||||
NETGUARDIA_DB_KEY: netguardia-dev-db-key
|
||||
NETGUARDIA_SECRETS_KEY: netguardia-dev-secrets-key
|
||||
dns:
|
||||
- 10.10.3.1
|
||||
- 8.8.8.8
|
||||
networks:
|
||||
mgmt-net:
|
||||
ipv4_address: 10.10.3.10
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /home/dalaw2/NetGuardia:/root/NetGuardia:z
|
||||
- /sys/fs/bpf:/sys/fs/bpf:rw
|
||||
- /sys/kernel/debug:/sys/kernel/debug:ro
|
||||
|
||||
router:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: compose/Containerfile.router
|
||||
image: netguardia-router:latest
|
||||
container_name: router
|
||||
hostname: router
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
network_mode: none
|
||||
|
||||
external:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: compose/Containerfile.endpoint
|
||||
image: netguardia-endpoint:latest
|
||||
container_name: external
|
||||
hostname: external
|
||||
cap_add:
|
||||
- NET_RAW
|
||||
- NET_ADMIN
|
||||
network_mode: none
|
||||
command: ["/opt/scripts/traffic-external.sh"]
|
||||
|
||||
internal:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: compose/Containerfile.endpoint
|
||||
image: netguardia-endpoint:latest
|
||||
container_name: internal
|
||||
hostname: internal
|
||||
cap_add:
|
||||
- NET_RAW
|
||||
- NET_ADMIN
|
||||
network_mode: none
|
||||
command: ["/opt/scripts/traffic-internal.sh"]
|
||||
|
||||
networks:
|
||||
mgmt-net:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 10.10.3.0/24
|
||||
gateway: 10.10.3.1
|
||||
14
deploy/logrotate.conf
Normal file
@ -0,0 +1,14 @@
|
||||
/var/log/netguardia/*.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
maxsize 500M
|
||||
create 0640 root root
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload netguardia.service 2>/dev/null || true
|
||||
endscript
|
||||
}
|
||||
38
deploy/netguardia.service
Normal file
@ -0,0 +1,38 @@
|
||||
[Unit]
|
||||
Description=NetGuardia Network Security Gateway
|
||||
Documentation=https://github.com/dalaw2/NetGuardia
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/opt/netguardia/bin/net-guardia
|
||||
WorkingDirectory=/opt/netguardia
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Watchdog: service must notify systemd within this interval or be killed
|
||||
WatchdogSec=30
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=false
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/opt/netguardia /var/log/netguardia /var/lib/netguardia
|
||||
PrivateTmp=yes
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
LimitMEMLOCK=infinity
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=netguardia
|
||||
|
||||
# Environment
|
||||
Environment=RUST_LOG=info
|
||||
Environment=CONFIG_PATH=/opt/netguardia/config.toml
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
46
deploy/packer/cloud-init/user-data
Normal file
@ -0,0 +1,46 @@
|
||||
#cloud-config
|
||||
autoinstall:
|
||||
version: 1
|
||||
locale: en_US.UTF-8
|
||||
keyboard:
|
||||
layout: us
|
||||
|
||||
identity:
|
||||
hostname: netguardia
|
||||
username: netguardia
|
||||
# Password: netguardia (mkpasswd --method=SHA-512)
|
||||
password: "$6$rounds=4096$randomsalt$PLACEHOLDER_HASH"
|
||||
|
||||
ssh:
|
||||
install-server: true
|
||||
allow-pw: true
|
||||
|
||||
storage:
|
||||
layout:
|
||||
name: lvm
|
||||
sizing-policy: all
|
||||
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
ens3:
|
||||
dhcp4: true
|
||||
|
||||
packages:
|
||||
- curl
|
||||
- net-tools
|
||||
- iproute2
|
||||
- linux-tools-common
|
||||
|
||||
late-commands:
|
||||
# Create required directories
|
||||
- mkdir -p /target/opt/netguardia/bin
|
||||
- mkdir -p /target/var/log/netguardia
|
||||
|
||||
# Enable serial console for headless access
|
||||
- >-
|
||||
curtin in-target -- systemctl enable serial-getty@ttyS0.service
|
||||
|
||||
final_message: |
|
||||
NetGuardia image provisioning complete.
|
||||
The HTTP setup wizard starts automatically on port 8080.
|
||||
262
deploy/packer/netguardia.pkr.hcl
Normal file
@ -0,0 +1,262 @@
|
||||
packer {
|
||||
required_plugins {
|
||||
qemu = {
|
||||
source = "github.com/hashicorp/qemu"
|
||||
version = ">= 1.1.0"
|
||||
}
|
||||
virtualbox = {
|
||||
source = "github.com/hashicorp/virtualbox"
|
||||
version = ">= 1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Variables
|
||||
# ---------------------------------------------------------------------------
|
||||
variable "ubuntu_iso_url" {
|
||||
type = string
|
||||
default = "https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso"
|
||||
}
|
||||
|
||||
variable "ubuntu_iso_checksum" {
|
||||
type = string
|
||||
description = "SHA-256 checksum of the Ubuntu 24.04 Server ISO (e.g. sha256:abcdef...). Must be provided explicitly."
|
||||
|
||||
validation {
|
||||
condition = can(regex("^sha256:[0-9a-fA-F]{64}$", var.ubuntu_iso_checksum))
|
||||
error_message = "ubuntu_iso_checksum must be a valid SHA-256 checksum in the form 'sha256:<64 hex chars>'. Do not use 'sha256:none'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "netguardia_binary" {
|
||||
type = string
|
||||
default = "../target/release/net-guardia"
|
||||
description = "Path to the pre-built NetGuardia binary."
|
||||
}
|
||||
|
||||
variable "ssh_username" {
|
||||
type = string
|
||||
default = "netguardia"
|
||||
}
|
||||
|
||||
variable "ssh_password" {
|
||||
type = string
|
||||
default = "netguardia"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "disk_size" {
|
||||
type = string
|
||||
default = "20480"
|
||||
description = "Virtual disk size in MB."
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
type = string
|
||||
default = "2048"
|
||||
}
|
||||
|
||||
variable "cpus" {
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
|
||||
variable "accelerator" {
|
||||
type = string
|
||||
default = "kvm"
|
||||
description = "QEMU accelerator: 'kvm' (default) or 'none' for environments without KVM support."
|
||||
|
||||
validation {
|
||||
condition = contains(["kvm", "none"], var.accelerator)
|
||||
error_message = "accelerator must be 'kvm' or 'none'."
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Source: QEMU (produces QCOW2)
|
||||
# ---------------------------------------------------------------------------
|
||||
source "qemu" "netguardia" {
|
||||
iso_url = var.ubuntu_iso_url
|
||||
iso_checksum = var.ubuntu_iso_checksum
|
||||
output_directory = "output-qcow2"
|
||||
format = "qcow2"
|
||||
disk_size = var.disk_size
|
||||
memory = var.memory
|
||||
cpus = var.cpus
|
||||
headless = true
|
||||
|
||||
ssh_username = var.ssh_username
|
||||
ssh_password = var.ssh_password
|
||||
ssh_timeout = "30m"
|
||||
|
||||
shutdown_command = "echo '${var.ssh_password}' | sudo -S shutdown -P now"
|
||||
boot_wait = "5s"
|
||||
|
||||
http_directory = "cloud-init"
|
||||
boot_command = [
|
||||
"c<wait>",
|
||||
"linux /casper/vmlinuz autoinstall ds='nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/' ",
|
||||
"--- <enter><wait>",
|
||||
"initrd /casper/initrd<enter><wait>",
|
||||
"boot<enter>"
|
||||
]
|
||||
|
||||
vm_name = "netguardia"
|
||||
net_device = "virtio-net"
|
||||
disk_interface = "virtio"
|
||||
accelerator = var.accelerator
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Source: VirtualBox (produces OVA)
|
||||
# ---------------------------------------------------------------------------
|
||||
source "virtualbox-iso" "netguardia" {
|
||||
iso_url = var.ubuntu_iso_url
|
||||
iso_checksum = var.ubuntu_iso_checksum
|
||||
output_directory = "output-ova"
|
||||
format = "ova"
|
||||
disk_size = var.disk_size
|
||||
memory = var.memory
|
||||
cpus = var.cpus
|
||||
headless = true
|
||||
guest_os_type = "Ubuntu_64"
|
||||
|
||||
ssh_username = var.ssh_username
|
||||
ssh_password = var.ssh_password
|
||||
ssh_timeout = "30m"
|
||||
|
||||
shutdown_command = "echo '${var.ssh_password}' | sudo -S shutdown -P now"
|
||||
boot_wait = "5s"
|
||||
|
||||
http_directory = "cloud-init"
|
||||
boot_command = [
|
||||
"c<wait>",
|
||||
"linux /casper/vmlinuz autoinstall ds='nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/' ",
|
||||
"--- <enter><wait>",
|
||||
"initrd /casper/initrd<enter><wait>",
|
||||
"boot<enter>"
|
||||
]
|
||||
|
||||
vboxmanage = [
|
||||
["modifyvm", "{{ .Name }}", "--nic2", "intnet"],
|
||||
["modifyvm", "{{ .Name }}", "--intnet2", "netguardia-internal"]
|
||||
]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build
|
||||
# ---------------------------------------------------------------------------
|
||||
build {
|
||||
sources = [
|
||||
"source.qemu.netguardia",
|
||||
"source.virtualbox-iso.netguardia"
|
||||
]
|
||||
|
||||
# ------ KVM fallback warning ------
|
||||
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"if [ '${var.accelerator}' = 'none' ]; then",
|
||||
" echo '⚠ WARNING: Building without KVM acceleration. This will be significantly slower.'",
|
||||
" echo '⚠ Set accelerator=kvm for production builds.'",
|
||||
"fi"
|
||||
]
|
||||
}
|
||||
|
||||
# ------ Upload artifacts ------
|
||||
|
||||
provisioner "file" {
|
||||
source = var.netguardia_binary
|
||||
destination = "/tmp/net-guardia"
|
||||
}
|
||||
|
||||
provisioner "file" {
|
||||
source = "../deploy/scripts/install.sh"
|
||||
destination = "/tmp/install.sh"
|
||||
}
|
||||
|
||||
provisioner "file" {
|
||||
source = "../deploy/netguardia.service"
|
||||
destination = "/tmp/netguardia.service"
|
||||
}
|
||||
|
||||
provisioner "file" {
|
||||
source = "../deploy/logrotate.conf"
|
||||
destination = "/tmp/logrotate.conf"
|
||||
}
|
||||
|
||||
provisioner "file" {
|
||||
source = "../deploy/setup-wizard.sh"
|
||||
destination = "/tmp/setup-wizard.sh"
|
||||
}
|
||||
|
||||
# ------ Debug binary gate ------
|
||||
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"set -e",
|
||||
"echo 'Checking binary is not a debug build...'",
|
||||
"if file /tmp/net-guardia | grep -q 'not stripped'; then",
|
||||
" echo 'FATAL: Binary is a debug build (not stripped). Use a release build for VM images.'",
|
||||
" exit 1",
|
||||
"fi",
|
||||
"echo 'Binary check passed: stripped release build.'"
|
||||
]
|
||||
}
|
||||
|
||||
# ------ Install runtime dependencies (SQLCipher needs OpenSSL) ------
|
||||
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"set -e",
|
||||
"if command -v apt-get &>/dev/null; then",
|
||||
" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libssl3",
|
||||
"elif command -v dnf &>/dev/null; then",
|
||||
" sudo dnf install -y openssl-libs",
|
||||
"fi"
|
||||
]
|
||||
}
|
||||
|
||||
# ------ Install via install.sh --local ------
|
||||
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"set -e",
|
||||
"chmod +x /tmp/install.sh",
|
||||
|
||||
"# Lay out deploy dir structure so install.sh can find service/logrotate files",
|
||||
"sudo mkdir -p /tmp/deploy/scripts",
|
||||
"cp /tmp/install.sh /tmp/deploy/scripts/install.sh",
|
||||
"cp /tmp/netguardia.service /tmp/deploy/netguardia.service",
|
||||
"cp /tmp/logrotate.conf /tmp/deploy/logrotate.conf",
|
||||
|
||||
"sudo /tmp/deploy/scripts/install.sh --local /tmp/net-guardia",
|
||||
|
||||
"# Install setup wizard",
|
||||
"sudo install -m 0755 /tmp/setup-wizard.sh /opt/netguardia/bin/setup-wizard.sh",
|
||||
|
||||
"# Configure first-boot setup wizard via rc.local",
|
||||
"sudo tee /etc/rc.local > /dev/null << 'RCEOF'",
|
||||
"#!/bin/bash",
|
||||
"if [ ! -f /opt/netguardia/config.toml ]; then",
|
||||
" /opt/netguardia/bin/setup-wizard.sh",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"RCEOF",
|
||||
"sudo chmod +x /etc/rc.local"
|
||||
]
|
||||
}
|
||||
|
||||
# ------ Final cleanup ------
|
||||
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"sudo apt-get -y autoremove",
|
||||
"sudo apt-get -y clean",
|
||||
"sudo rm -rf /tmp/* /var/tmp/*",
|
||||
"sudo truncate -s 0 /var/log/syslog",
|
||||
"history -c"
|
||||
]
|
||||
}
|
||||
}
|
||||
522
deploy/scripts/dev.sh
Executable file
@ -0,0 +1,522 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the NetGuardia development containers and inline veth topology.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEPLOY_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
ROOT_DIR="$(cd "$DEPLOY_DIR/.." && pwd)"
|
||||
BASE_COMPOSE_FILE="$DEPLOY_DIR/compose/podman-compose.yml"
|
||||
COMPOSE_FILE="/tmp/netguardia-compose-$$.yml"
|
||||
COMPOSE_PROJECT="compose"
|
||||
LOG_FILE="/tmp/netguardia-dev-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
VERBOSE=0
|
||||
CLEANUP_FIRST=1
|
||||
RT=""
|
||||
declare -a RT_CMD=()
|
||||
declare -a COMPOSE_CMD=()
|
||||
|
||||
info() {
|
||||
printf '[INFO] %s\n' "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[WARN] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
fatal() {
|
||||
printf '[ERROR] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: sudo bash deploy/scripts/dev.sh [--verbose] [--no-cleanup]
|
||||
|
||||
Options:
|
||||
--verbose Print compose build/up output in addition to writing the log.
|
||||
--no-cleanup Skip the default preflight cleanup of old containers/veth links.
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--verbose)
|
||||
VERBOSE=1
|
||||
shift
|
||||
;;
|
||||
--no-cleanup)
|
||||
CLEANUP_FIRST=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fatal "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cleanup_netns_links() {
|
||||
rm -f \
|
||||
/var/run/netns/external \
|
||||
/var/run/netns/internal \
|
||||
/var/run/netns/router \
|
||||
/var/run/netns/netguardia
|
||||
}
|
||||
|
||||
cleanup_temp_files() {
|
||||
cleanup_netns_links
|
||||
rm -f "$COMPOSE_FILE"
|
||||
}
|
||||
|
||||
trap cleanup_temp_files EXIT
|
||||
|
||||
require_root() {
|
||||
[[ "$(id -u)" -eq 0 ]] || fatal "dev.sh must run as root. Use: sudo bash deploy/scripts/dev.sh"
|
||||
}
|
||||
|
||||
require_linux_host() {
|
||||
[[ "$(uname -s)" == "Linux" ]] || fatal "dev.sh supports Linux hosts only."
|
||||
if grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then
|
||||
fatal "WSL2 is not supported for this XDP/AF_XDP development topology."
|
||||
fi
|
||||
}
|
||||
|
||||
package_manager() {
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
printf 'dnf'
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
printf 'apt-get'
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
printf 'zypper'
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
printf 'pacman'
|
||||
fi
|
||||
}
|
||||
|
||||
package_for_command() {
|
||||
local manager="$1"
|
||||
local command_name="$2"
|
||||
|
||||
case "$manager:$command_name" in
|
||||
dnf:ip) printf 'iproute' ;;
|
||||
dnf:ping) printf 'iputils' ;;
|
||||
dnf:ethtool) printf 'ethtool' ;;
|
||||
dnf:curl) printf 'curl' ;;
|
||||
dnf:ln|dnf:mkdir|dnf:rm|dnf:uname) printf 'coreutils' ;;
|
||||
apt-get:ip) printf 'iproute2' ;;
|
||||
apt-get:ping) printf 'iputils-ping' ;;
|
||||
apt-get:ethtool) printf 'ethtool' ;;
|
||||
apt-get:curl) printf 'curl' ;;
|
||||
apt-get:ln|apt-get:mkdir|apt-get:rm|apt-get:uname) printf 'coreutils' ;;
|
||||
zypper:ip) printf 'iproute2' ;;
|
||||
zypper:ping) printf 'iputils' ;;
|
||||
zypper:ethtool) printf 'ethtool' ;;
|
||||
zypper:curl) printf 'curl' ;;
|
||||
zypper:ln|zypper:mkdir|zypper:rm|zypper:uname) printf 'coreutils' ;;
|
||||
pacman:ip) printf 'iproute2' ;;
|
||||
pacman:ping) printf 'iputils' ;;
|
||||
pacman:ethtool) printf 'ethtool' ;;
|
||||
pacman:curl) printf 'curl' ;;
|
||||
pacman:ln|pacman:mkdir|pacman:rm|pacman:uname) printf 'coreutils' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
append_unique() {
|
||||
local value="$1"
|
||||
shift
|
||||
local existing
|
||||
|
||||
for existing in "$@"; do
|
||||
[[ "$existing" == "$value" ]] && return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
install_packages() {
|
||||
local manager="$1"
|
||||
shift
|
||||
|
||||
case "$manager" in
|
||||
dnf)
|
||||
dnf install -y "$@"
|
||||
;;
|
||||
apt-get)
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
|
||||
;;
|
||||
zypper)
|
||||
zypper --non-interactive install "$@"
|
||||
;;
|
||||
pacman)
|
||||
pacman -Sy --noconfirm "$@"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
manual_install_command() {
|
||||
local manager="$1"
|
||||
shift
|
||||
|
||||
case "$manager" in
|
||||
dnf) printf 'dnf install -y %s\n' "$*" ;;
|
||||
apt-get) printf 'apt-get update && apt-get install -y %s\n' "$*" ;;
|
||||
zypper) printf 'zypper --non-interactive install %s\n' "$*" ;;
|
||||
pacman) printf 'pacman -Sy --noconfirm %s\n' "$*" ;;
|
||||
*) printf 'Install packages manually: %s\n' "$*" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_host_tools() {
|
||||
local required_commands=(ip ping ethtool curl ln mkdir rm uname)
|
||||
local missing_commands=()
|
||||
local packages=()
|
||||
local command_name manager package answer
|
||||
|
||||
for command_name in "${required_commands[@]}"; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
missing_commands+=("$command_name")
|
||||
fi
|
||||
done
|
||||
|
||||
if ((${#missing_commands[@]} == 0)); then
|
||||
info "Host tools OK"
|
||||
return 0
|
||||
fi
|
||||
|
||||
manager="$(package_manager || true)"
|
||||
[[ -n "$manager" ]] || fatal "Missing host commands: ${missing_commands[*]}. No supported package manager found."
|
||||
|
||||
for command_name in "${missing_commands[@]}"; do
|
||||
package="$(package_for_command "$manager" "$command_name")"
|
||||
[[ -n "$package" ]] || fatal "No package mapping for missing command '$command_name' on $manager."
|
||||
if append_unique "$package" "${packages[@]}"; then
|
||||
packages+=("$package")
|
||||
fi
|
||||
done
|
||||
|
||||
warn "Missing host commands: ${missing_commands[*]}"
|
||||
warn "Package manager: $manager"
|
||||
warn "Packages to install: ${packages[*]}"
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
manual_install_command "$manager" "${packages[@]}" >&2
|
||||
fatal "Non-interactive shell; refusing to install packages without consent."
|
||||
fi
|
||||
|
||||
read -r -p "Install missing packages? [y/N] " answer
|
||||
case "$answer" in
|
||||
y|Y|yes|YES)
|
||||
install_packages "$manager" "${packages[@]}"
|
||||
;;
|
||||
*)
|
||||
manual_install_command "$manager" "${packages[@]}" >&2
|
||||
fatal "Required host packages were not installed."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
runtime_install_hint() {
|
||||
cat >&2 <<'EOF'
|
||||
Install a supported container runtime first.
|
||||
|
||||
Examples:
|
||||
dnf install -y podman podman-compose
|
||||
apt-get install -y podman podman-compose
|
||||
apt-get install -y docker.io docker-compose-plugin
|
||||
EOF
|
||||
}
|
||||
|
||||
check_docker_supported() {
|
||||
local context security_options operating_system
|
||||
|
||||
context="$(docker context show 2>/dev/null || true)"
|
||||
if [[ "$context" == "desktop-linux" ]]; then
|
||||
fatal "Docker Desktop is not supported for this XDP/netns topology."
|
||||
fi
|
||||
|
||||
operating_system="$(docker info --format '{{.OperatingSystem}}' 2>/dev/null || true)"
|
||||
if [[ "$operating_system" == *"Docker Desktop"* ]]; then
|
||||
fatal "Docker Desktop is not supported for this XDP/netns topology."
|
||||
fi
|
||||
|
||||
security_options="$(docker info --format '{{json .SecurityOptions}}' 2>/dev/null || true)"
|
||||
if grep -qi rootless <<<"$security_options"; then
|
||||
fatal "Rootless Docker is not supported for this privileged XDP/netns topology."
|
||||
fi
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if command -v podman-compose >/dev/null 2>&1 && command -v podman >/dev/null 2>&1; then
|
||||
RT="podman"
|
||||
RT_CMD=(podman)
|
||||
COMPOSE_CMD=(podman-compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE")
|
||||
elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
check_docker_supported
|
||||
RT="docker"
|
||||
RT_CMD=(docker)
|
||||
COMPOSE_CMD=(docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE")
|
||||
else
|
||||
runtime_install_hint
|
||||
fatal "No supported runtime found. Need podman + podman-compose or docker + docker compose."
|
||||
fi
|
||||
|
||||
info "Runtime: $RT"
|
||||
}
|
||||
|
||||
generate_compose_file() {
|
||||
local yaml_deploy
|
||||
local yaml_root
|
||||
|
||||
[[ -f "$BASE_COMPOSE_FILE" ]] || fatal "Compose file not found: $BASE_COMPOSE_FILE"
|
||||
yaml_deploy="${DEPLOY_DIR//\'/\'\'}"
|
||||
yaml_root="${ROOT_DIR//\'/\'\'}"
|
||||
|
||||
: >"$COMPOSE_FILE"
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
" context: ..")
|
||||
printf " context: '%s'\n" "$yaml_deploy" >>"$COMPOSE_FILE"
|
||||
;;
|
||||
" - /home/dalaw2/NetGuardia:/root/NetGuardia:z")
|
||||
printf " - '%s:/root/NetGuardia:z'\n" "$yaml_root" >>"$COMPOSE_FILE"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$line" >>"$COMPOSE_FILE"
|
||||
;;
|
||||
esac
|
||||
done <"$BASE_COMPOSE_FILE"
|
||||
}
|
||||
|
||||
run_logged() {
|
||||
local label="$1"
|
||||
shift
|
||||
|
||||
info "$label"
|
||||
if ((VERBOSE)); then
|
||||
"$@" 2>&1 | tee -a "$LOG_FILE"
|
||||
elif [[ -t 1 ]]; then
|
||||
run_with_spinner "$label" "$@"
|
||||
elif ! "$@" >>"$LOG_FILE" 2>&1; then
|
||||
warn "$label failed. Last log lines:"
|
||||
tail -n 80 "$LOG_FILE" >&2 || true
|
||||
fatal "Full log: $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
run_with_spinner() {
|
||||
local label="$1"
|
||||
shift
|
||||
local pid status
|
||||
|
||||
"$@" >>"$LOG_FILE" 2>&1 &
|
||||
pid=$!
|
||||
spinner "$pid" "$label"
|
||||
set +e
|
||||
wait "$pid"
|
||||
status=$?
|
||||
set -e
|
||||
clear_spinner_line
|
||||
if ((status != 0)); then
|
||||
warn "$label failed. Last log lines:"
|
||||
tail -n 80 "$LOG_FILE" >&2 || true
|
||||
fatal "Full log: $LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
spinner() {
|
||||
local pid="$1"
|
||||
local label="$2"
|
||||
local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
||||
local i=0
|
||||
local frame
|
||||
local started_at=$SECONDS
|
||||
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
frame="${frames:i++%${#frames}:1}"
|
||||
printf '\r%s %s... %02ds' "$frame" "$label" "$((SECONDS - started_at))"
|
||||
sleep 0.12
|
||||
done
|
||||
}
|
||||
|
||||
clear_spinner_line() {
|
||||
printf '\r\033[K'
|
||||
}
|
||||
|
||||
runtime_rm_containers() {
|
||||
"${RT_CMD[@]}" rm -f netguardia router external internal >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
delete_host_link() {
|
||||
local link_name="$1"
|
||||
ip link del "$link_name" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
preflight_cleanup() {
|
||||
((CLEANUP_FIRST)) || return 0
|
||||
|
||||
info "Cleaning old development topology"
|
||||
runtime_rm_containers
|
||||
cleanup_netns_links
|
||||
delete_host_link ext-eth0
|
||||
delete_host_link rtr-ext
|
||||
delete_host_link rtr-int
|
||||
delete_host_link ng-ext
|
||||
delete_host_link int-eth0
|
||||
delete_host_link ng-int
|
||||
}
|
||||
|
||||
container_pid() {
|
||||
"${RT_CMD[@]}" inspect --format '{{.State.Pid}}' "$1"
|
||||
}
|
||||
|
||||
link_netns() {
|
||||
local container="$1"
|
||||
local pid
|
||||
|
||||
pid="$(container_pid "$container")"
|
||||
[[ -n "$pid" && "$pid" != "0" ]] || fatal "Container '$container' is not running."
|
||||
ln -sf "/proc/$pid/ns/net" "/var/run/netns/$container"
|
||||
}
|
||||
|
||||
link_container_namespaces() {
|
||||
mkdir -p /var/run/netns
|
||||
link_netns external
|
||||
link_netns internal
|
||||
link_netns router
|
||||
link_netns netguardia
|
||||
}
|
||||
|
||||
netns() {
|
||||
ip netns exec "$@"
|
||||
}
|
||||
|
||||
disable_offload() {
|
||||
local namespace="$1"
|
||||
local interface="$2"
|
||||
|
||||
netns "$namespace" ethtool -K "$interface" tx off rx off >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
create_topology() {
|
||||
info "Creating inline veth topology"
|
||||
|
||||
ip link add ext-eth0 type veth peer name rtr-ext
|
||||
ip link set ext-eth0 netns external
|
||||
ip link set rtr-ext netns router
|
||||
|
||||
netns external ip link set lo up
|
||||
netns external ip link set ext-eth0 up
|
||||
netns external ip addr add 10.10.1.2/24 dev ext-eth0
|
||||
for i in 3 4 5 6 7; do
|
||||
netns external ip addr add "10.10.1.$i/24" dev ext-eth0
|
||||
done
|
||||
netns external ip route replace default via 10.10.1.1
|
||||
|
||||
netns router ip link set lo up
|
||||
netns router ip link set rtr-ext up
|
||||
netns router ip addr add 10.10.1.1/24 dev rtr-ext
|
||||
|
||||
ip link add rtr-int type veth peer name ng-ext
|
||||
ip link set rtr-int netns router
|
||||
ip link set ng-ext netns netguardia
|
||||
|
||||
ip link add int-eth0 type veth peer name ng-int
|
||||
ip link set int-eth0 netns internal
|
||||
ip link set ng-int netns netguardia
|
||||
|
||||
netns router ip link set rtr-int up
|
||||
netns router ip addr add 10.10.2.1/24 dev rtr-int
|
||||
netns router sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
|
||||
|
||||
netns internal ip link set lo up
|
||||
netns internal ip link set int-eth0 up
|
||||
netns internal ip addr add 10.10.2.2/24 dev int-eth0
|
||||
for i in 3 4 5 6; do
|
||||
netns internal ip addr add "10.10.2.$i/24" dev int-eth0
|
||||
done
|
||||
netns internal ip route replace default via 10.10.2.1
|
||||
|
||||
netns netguardia ip link set ng-ext up
|
||||
netns netguardia ip link set ng-int up
|
||||
|
||||
disable_offload router rtr-int
|
||||
disable_offload router rtr-ext
|
||||
disable_offload internal int-eth0
|
||||
disable_offload external ext-eth0
|
||||
disable_offload netguardia ng-ext
|
||||
disable_offload netguardia ng-int
|
||||
}
|
||||
|
||||
write_interface_mapping() {
|
||||
cat >/tmp/netguardia_interfaces.txt <<'IEOF'
|
||||
# NetGuardia interface mapping - realistic inline deployment
|
||||
# Router handles L3 (10.10.1.0/24 <-> 10.10.2.0/24)
|
||||
# NetGuardia inline on 10.10.2.0/24 (no IP, no bridge)
|
||||
# ng-ext - XDP ingress (router side, attached to rtr-int peer)
|
||||
# ng-int - XDP egress (internal side, attached to int-eth0 peer)
|
||||
# XSK forwards packets: ng-ext RX -> ng-int TX and ng-int RX -> ng-ext TX
|
||||
# Management: eth0 (10.10.3.10)
|
||||
IEOF
|
||||
"${RT_CMD[@]}" cp /tmp/netguardia_interfaces.txt netguardia:/root/NetGuardia/interfaces.txt >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
connectivity_check() {
|
||||
local external_router="FAIL"
|
||||
|
||||
if netns external ping -c 1 -W 2 10.10.1.1 >/dev/null 2>&1; then
|
||||
external_router="OK"
|
||||
fi
|
||||
|
||||
info "Connectivity: external -> router: $external_router"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
cat <<EOF
|
||||
|
||||
NetGuardia development topology is ready.
|
||||
|
||||
Mgmt: http://<host-ip>:8080
|
||||
|
||||
external (10.10.1.{2-7}) -> router -> ng-ext
|
||||
ng-ext <-> net-guardia XSK <-> ng-int
|
||||
ng-int -> internal (10.10.2.{2-6})
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
require_root
|
||||
require_linux_host
|
||||
check_host_tools
|
||||
generate_compose_file
|
||||
detect_runtime
|
||||
preflight_cleanup
|
||||
|
||||
: >"$LOG_FILE"
|
||||
info "Compose log: $LOG_FILE"
|
||||
run_logged "Building containers" "${COMPOSE_CMD[@]}" build
|
||||
generate_compose_file
|
||||
run_logged "Starting containers" "${COMPOSE_CMD[@]}" up -d
|
||||
|
||||
info "Containers running"
|
||||
"${RT_CMD[@]}" ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null || "${RT_CMD[@]}" ps
|
||||
|
||||
link_container_namespaces
|
||||
create_topology
|
||||
write_interface_mapping
|
||||
connectivity_check
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
134
deploy/scripts/install.sh
Executable file
@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
# install.sh — Install NetGuardia on a fresh system.
|
||||
#
|
||||
# Usage:
|
||||
# install.sh # Download from GitHub Release
|
||||
# install.sh --local /path/to/binary # Use a pre-built local binary
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; }
|
||||
fatal() { printf '\033[1;31m[FATAL]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ── Defaults ─────────────────────────────────────────────────────────────────
|
||||
LOCAL_BINARY=""
|
||||
INSTALL_DIR="/opt/netguardia"
|
||||
BIN_DIR="${INSTALL_DIR}/bin"
|
||||
DATA_DIR="/var/lib/netguardia"
|
||||
LOG_DIR="/var/log/netguardia"
|
||||
SERVICE_USER="netguardia"
|
||||
SERVICE_GROUP="netguardia"
|
||||
GITHUB_REPO="dalaw2/NetGuardia"
|
||||
|
||||
# ── Parse arguments ──────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--local)
|
||||
[[ -z "${2:-}" ]] && fatal "--local requires a path to the binary"
|
||||
LOCAL_BINARY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--local /path/to/binary]"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fatal "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Validate local binary (if provided) ─────────────────────────────────────
|
||||
if [[ -n "${LOCAL_BINARY}" ]]; then
|
||||
[[ -f "${LOCAL_BINARY}" ]] || fatal "Local binary not found: ${LOCAL_BINARY}"
|
||||
[[ -x "${LOCAL_BINARY}" ]] || fatal "Local binary is not executable: ${LOCAL_BINARY}"
|
||||
info "Using local binary: ${LOCAL_BINARY}"
|
||||
fi
|
||||
|
||||
# ── Must be root ─────────────────────────────────────────────────────────────
|
||||
[[ "$(id -u)" -eq 0 ]] || fatal "This script must be run as root"
|
||||
|
||||
# ── Install runtime dependencies (SQLCipher needs OpenSSL) ──────────────────
|
||||
if command -v apt-get &>/dev/null; then
|
||||
info "Refreshing apt package metadata"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update >/dev/null 2>&1 || warn "Could not refresh apt metadata"
|
||||
info "Installing runtime dependencies (libssl)"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y libssl3 >/dev/null 2>&1 || warn "Could not install libssl3"
|
||||
elif command -v dnf &>/dev/null; then
|
||||
info "Installing runtime dependencies (openssl-libs)"
|
||||
dnf install -y openssl-libs >/dev/null 2>&1 || warn "Could not install openssl-libs"
|
||||
fi
|
||||
|
||||
# ── Create system user ───────────────────────────────────────────────────────
|
||||
if ! id "${SERVICE_USER}" &>/dev/null; then
|
||||
info "Creating system user: ${SERVICE_USER}"
|
||||
useradd --system --no-create-home --shell /usr/sbin/nologin "${SERVICE_USER}"
|
||||
fi
|
||||
|
||||
# ── Create directories ───────────────────────────────────────────────────────
|
||||
info "Creating directories"
|
||||
mkdir -p "${BIN_DIR}" "${DATA_DIR}" "${LOG_DIR}"
|
||||
chown "${SERVICE_USER}:${SERVICE_GROUP}" "${DATA_DIR}" "${LOG_DIR}"
|
||||
|
||||
# ── Obtain the binary ────────────────────────────────────────────────────────
|
||||
if [[ -n "${LOCAL_BINARY}" ]]; then
|
||||
# --local mode: skip download and checksum entirely
|
||||
info "Installing local binary to ${BIN_DIR}/net-guardia"
|
||||
install -m 0755 "${LOCAL_BINARY}" "${BIN_DIR}/net-guardia"
|
||||
else
|
||||
# Download from GitHub Release
|
||||
info "Fetching latest release from GitHub (${GITHUB_REPO})"
|
||||
LATEST_TAG=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" \
|
||||
| grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
[[ -n "${LATEST_TAG}" ]] || fatal "Could not determine latest release tag"
|
||||
info "Latest release: ${LATEST_TAG}"
|
||||
|
||||
DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_TAG}/net-guardia-linux-amd64"
|
||||
CHECKSUMS_URL="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_TAG}/SHA256SUMS"
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "${TMPDIR}"' EXIT
|
||||
|
||||
info "Downloading binary"
|
||||
curl -fSL -o "${TMPDIR}/net-guardia" "${DOWNLOAD_URL}"
|
||||
|
||||
info "Downloading SHA256SUMS"
|
||||
if ! curl -fSL -o "${TMPDIR}/SHA256SUMS" "${CHECKSUMS_URL}"; then
|
||||
fatal "SHA256SUMS file not found in release — aborting"
|
||||
fi
|
||||
|
||||
info "Verifying checksum"
|
||||
(cd "${TMPDIR}" && sha256sum -c SHA256SUMS)
|
||||
|
||||
install -m 0755 "${TMPDIR}/net-guardia" "${BIN_DIR}/net-guardia"
|
||||
fi
|
||||
|
||||
# ── Install systemd unit ─────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
if [[ -f "${DEPLOY_DIR}/netguardia.service" ]]; then
|
||||
info "Installing systemd unit"
|
||||
install -m 0644 "${DEPLOY_DIR}/netguardia.service" /etc/systemd/system/netguardia.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable netguardia.service
|
||||
else
|
||||
warn "netguardia.service not found at ${DEPLOY_DIR}/netguardia.service — skipping"
|
||||
fi
|
||||
|
||||
# ── Install logrotate config ─────────────────────────────────────────────────
|
||||
if [[ -f "${DEPLOY_DIR}/logrotate.conf" ]]; then
|
||||
info "Installing logrotate config"
|
||||
install -m 0644 "${DEPLOY_DIR}/logrotate.conf" /etc/logrotate.d/netguardia
|
||||
else
|
||||
warn "logrotate.conf not found — skipping"
|
||||
fi
|
||||
|
||||
# ── Done ─────────────────────────────────────────────────────────────────────
|
||||
info "NetGuardia installed successfully"
|
||||
info " Binary: ${BIN_DIR}/net-guardia"
|
||||
info " Data: ${DATA_DIR}"
|
||||
info " Logs: ${LOG_DIR}"
|
||||
info " Service: systemctl start netguardia"
|
||||
127
deploy/scripts/traffic-external.sh
Normal file
@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# External network traffic generator
|
||||
# Simulates external hosts sending traffic toward the internal network
|
||||
|
||||
INTERNAL_IP="10.10.2.2"
|
||||
SELF_BASE="10.10.1"
|
||||
|
||||
echo "[external] Traffic generator started"
|
||||
|
||||
# Wait for network and routing to be ready
|
||||
sleep 5
|
||||
until ping -c 1 -W 1 $INTERNAL_IP &>/dev/null; do
|
||||
echo "[external] Waiting for internal connectivity..."
|
||||
sleep 2
|
||||
done
|
||||
echo "[external] Internal network reachable"
|
||||
|
||||
# --- Benign traffic functions ---
|
||||
|
||||
http_traffic() {
|
||||
while true; do
|
||||
for src in 2 3 4; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
# Various HTTP methods
|
||||
curl -s --interface $ip -o /dev/null -m 3 http://${INTERNAL_IP}/ 2>/dev/null
|
||||
curl -s --interface $ip -o /dev/null -m 3 -X POST -d "data=test" http://${INTERNAL_IP}/ 2>/dev/null
|
||||
curl -s --interface $ip -o /dev/null -m 3 -X HEAD http://${INTERNAL_IP}/ 2>/dev/null
|
||||
curl -s --interface $ip -o /dev/null -m 3 -X OPTIONS http://${INTERNAL_IP}/ 2>/dev/null
|
||||
done
|
||||
sleep $((RANDOM % 3 + 1))
|
||||
done
|
||||
}
|
||||
|
||||
ssh_traffic() {
|
||||
while true; do
|
||||
for src in 2 5; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
# SSH connection attempts (will fail but generates TCP SYN to port 22)
|
||||
timeout 2 bash -c "echo | nc -w 1 -s $ip $INTERNAL_IP 22" 2>/dev/null || true
|
||||
done
|
||||
sleep $((RANDOM % 5 + 3))
|
||||
done
|
||||
}
|
||||
|
||||
dns_traffic() {
|
||||
while true; do
|
||||
for src in 2 3 6; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
# UDP packets to port 53 (DNS-like)
|
||||
echo -ne '\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\x01\x00\x01' | \
|
||||
nc -u -w 1 -s $ip $INTERNAL_IP 53 2>/dev/null || true
|
||||
done
|
||||
sleep $((RANDOM % 4 + 2))
|
||||
done
|
||||
}
|
||||
|
||||
udp_traffic() {
|
||||
while true; do
|
||||
for src in 2 4 7; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
for port in 5000 5001 8000 9090; do
|
||||
echo "udp-payload-$(date +%s)" | nc -u -w 1 -s $ip $INTERNAL_IP $port 2>/dev/null || true
|
||||
done
|
||||
done
|
||||
sleep $((RANDOM % 3 + 2))
|
||||
done
|
||||
}
|
||||
|
||||
tcp_traffic() {
|
||||
while true; do
|
||||
for src in 3 5 6; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
for port in 3000 4000 6379 5432; do
|
||||
timeout 2 bash -c "echo 'hello' | nc -w 1 -s $ip $INTERNAL_IP $port" 2>/dev/null || true
|
||||
done
|
||||
done
|
||||
sleep $((RANDOM % 4 + 2))
|
||||
done
|
||||
}
|
||||
|
||||
icmp_traffic() {
|
||||
while true; do
|
||||
for src in 2 3 4 5; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
ping -c 2 -W 1 -I $ip $INTERNAL_IP >/dev/null 2>&1 || true
|
||||
done
|
||||
sleep $((RANDOM % 5 + 3))
|
||||
done
|
||||
}
|
||||
|
||||
# --- Attack-like traffic (low intensity, for ML training) ---
|
||||
|
||||
syn_scan() {
|
||||
while true; do
|
||||
sleep $((RANDOM % 30 + 30))
|
||||
src="${SELF_BASE}.$((RANDOM % 3 + 5))"
|
||||
echo "[external] SYN scan burst from $src"
|
||||
# Quick port scan pattern
|
||||
for port in 22 80 443 8080 3306 5432 6379 8443 9090 27017; do
|
||||
timeout 1 bash -c "echo | nc -w 1 -s $src $INTERNAL_IP $port" 2>/dev/null || true
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
udp_burst() {
|
||||
while true; do
|
||||
sleep $((RANDOM % 60 + 45))
|
||||
src="${SELF_BASE}.$((RANDOM % 3 + 5))"
|
||||
echo "[external] UDP burst from $src"
|
||||
for i in $(seq 1 50); do
|
||||
echo "flood-$i" | nc -u -w 0 -s $src $INTERNAL_IP $((RANDOM % 10000 + 1024)) 2>/dev/null || true
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# Launch all traffic generators in background
|
||||
http_traffic &
|
||||
ssh_traffic &
|
||||
dns_traffic &
|
||||
udp_traffic &
|
||||
tcp_traffic &
|
||||
icmp_traffic &
|
||||
syn_scan &
|
||||
udp_burst &
|
||||
|
||||
echo "[external] All traffic generators running"
|
||||
wait
|
||||
90
deploy/scripts/traffic-internal.sh
Normal file
@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# Internal network traffic generator
|
||||
# Simulates internal hosts sending traffic toward the external network
|
||||
# Also runs services (HTTP, SSH) for external to connect to
|
||||
|
||||
EXTERNAL_IP="10.10.1.2"
|
||||
SELF_BASE="10.10.2"
|
||||
|
||||
echo "[internal] Traffic generator started"
|
||||
|
||||
# Start HTTP server on all interfaces
|
||||
httpd -D FOREGROUND &
|
||||
HTTPD_PID=$!
|
||||
|
||||
# Start a simple SSH listener (for connection pattern generation)
|
||||
ssh-keygen -A 2>/dev/null
|
||||
/usr/sbin/sshd 2>/dev/null || true
|
||||
|
||||
# Wait for network to be ready
|
||||
sleep 3
|
||||
until ping -c 1 -W 1 $EXTERNAL_IP &>/dev/null; do
|
||||
echo "[internal] Waiting for external connectivity..."
|
||||
sleep 2
|
||||
done
|
||||
echo "[internal] External network reachable"
|
||||
|
||||
# --- Benign outbound traffic ---
|
||||
|
||||
http_outbound() {
|
||||
while true; do
|
||||
for src in 2 3 4; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
curl -s --interface $ip -o /dev/null -m 3 http://${EXTERNAL_IP}/ 2>/dev/null
|
||||
curl -s --interface $ip -o /dev/null -m 3 -X POST -d "query=test" http://${EXTERNAL_IP}/ 2>/dev/null
|
||||
done
|
||||
sleep $((RANDOM % 4 + 2))
|
||||
done
|
||||
}
|
||||
|
||||
dns_outbound() {
|
||||
while true; do
|
||||
for src in 2 5; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
echo -ne '\x00\x02\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' | \
|
||||
nc -u -w 1 -s $ip $EXTERNAL_IP 53 2>/dev/null || true
|
||||
done
|
||||
sleep $((RANDOM % 5 + 3))
|
||||
done
|
||||
}
|
||||
|
||||
udp_outbound() {
|
||||
while true; do
|
||||
for src in 3 4 6; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
echo "internal-udp-$(date +%s)" | nc -u -w 1 -s $ip $EXTERNAL_IP $((RANDOM % 5000 + 5000)) 2>/dev/null || true
|
||||
done
|
||||
sleep $((RANDOM % 4 + 2))
|
||||
done
|
||||
}
|
||||
|
||||
tcp_outbound() {
|
||||
while true; do
|
||||
for src in 2 5 6; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
timeout 2 bash -c "echo 'ping' | nc -w 1 -s $ip $EXTERNAL_IP $((RANDOM % 1000 + 3000))" 2>/dev/null || true
|
||||
done
|
||||
sleep $((RANDOM % 5 + 3))
|
||||
done
|
||||
}
|
||||
|
||||
icmp_outbound() {
|
||||
while true; do
|
||||
for src in 2 3; do
|
||||
local ip="${SELF_BASE}.${src}"
|
||||
ping -c 1 -W 1 -I $ip $EXTERNAL_IP >/dev/null 2>&1 || true
|
||||
done
|
||||
sleep $((RANDOM % 6 + 4))
|
||||
done
|
||||
}
|
||||
|
||||
# Launch all traffic generators
|
||||
http_outbound &
|
||||
dns_outbound &
|
||||
udp_outbound &
|
||||
tcp_outbound &
|
||||
icmp_outbound &
|
||||
|
||||
echo "[internal] All traffic generators + services running"
|
||||
echo "[internal] Services: HTTP(:80), SSH(:22)"
|
||||
wait
|
||||
5
egress-ebpf/.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[build]
|
||||
target = ["bpfeb-unknown-none", "bpfel-unknown-none"]
|
||||
|
||||
[unstable]
|
||||
build-std = ["core"]
|
||||
@ -1,14 +1,12 @@
|
||||
[package]
|
||||
name = "net-guardia-egress-ebpf"
|
||||
version = "0.1.0"
|
||||
name = "egress-ebpf"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
net-guardia-common = { path = "../net-guardia-common" }
|
||||
|
||||
net-guardia-abi = { workspace = true, features = ["kernel"] }
|
||||
aya-ebpf = { workspace = true }
|
||||
aya-log-ebpf = { workspace = true }
|
||||
network-types = "0.0.7"
|
||||
|
||||
[build-dependencies]
|
||||
which = { workspace = true }
|
||||
@ -16,3 +14,6 @@ which = { workspace = true }
|
||||
[[bin]]
|
||||
name = "net-guardia-egress"
|
||||
path = "src/main.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
bench = false
|
||||
5
egress-ebpf/build.rs
Normal file
@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
if let Ok(linker) = which::which("bpf-linker") {
|
||||
println!("cargo:rerun-if-changed={}", linker.display());
|
||||
}
|
||||
}
|
||||
1
egress-ebpf/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
#![no_std]
|
||||
43
egress-ebpf/src/main.rs
Normal file
@ -0,0 +1,43 @@
|
||||
#![cfg_attr(any(target_arch = "bpf", target_os = "none"), no_std)]
|
||||
#![no_main]
|
||||
|
||||
use aya_ebpf::bindings::xdp_action;
|
||||
use aya_ebpf::macros::{map, xdp};
|
||||
use aya_ebpf::maps::{Array, XskMap};
|
||||
use aya_ebpf::programs::XdpContext;
|
||||
#[allow(unused_imports)]
|
||||
use aya_log_ebpf::info;
|
||||
use net_guardia_abi::ebpf::parsing;
|
||||
use net_guardia_abi::ebpf::symmetric_hash::symmetric_queue_id;
|
||||
use net_guardia_abi::model::parsed_packet::ParsedPacket;
|
||||
|
||||
#[map]
|
||||
static NUM_QUEUES: Array<u32> = Array::with_max_entries(1, 0);
|
||||
|
||||
#[map]
|
||||
static EGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0);
|
||||
|
||||
#[xdp]
|
||||
pub fn net_guardia(ctx: XdpContext) -> u32 {
|
||||
let queue_id = unsafe { compute_symmetric_queue_id(&ctx).unwrap_or((*ctx.ctx).rx_queue_index) };
|
||||
match EGRESS_XSKS_MAP.redirect(queue_id, 0) {
|
||||
Ok(action) => action,
|
||||
Err(_) => xdp_action::XDP_PASS,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn compute_symmetric_queue_id(ctx: &XdpContext) -> Option<u32> {
|
||||
unsafe {
|
||||
let mut pkt = core::mem::zeroed::<ParsedPacket>();
|
||||
parsing::parse_packet(ctx.data(), ctx.data_end(), &mut pkt)?;
|
||||
let num_q = *NUM_QUEUES.get(0)?;
|
||||
symmetric_queue_id(&pkt, num_q)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(test), any(target_arch = "bpf", target_os = "none")))]
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
unsafe { core::hint::unreachable_unchecked() }
|
||||
}
|
||||
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 130 KiB |
5
ingress-ebpf/.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[build]
|
||||
target = ["bpfeb-unknown-none", "bpfel-unknown-none"]
|
||||
|
||||
[unstable]
|
||||
build-std = ["core"]
|
||||
@ -1,14 +1,13 @@
|
||||
[package]
|
||||
name = "net-guardia-ingress-ebpf"
|
||||
version = "0.1.0"
|
||||
name = "ingress-ebpf"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
net-guardia-common = { path = "../net-guardia-common" }
|
||||
|
||||
net-guardia-abi = { workspace = true, features = ["kernel"] }
|
||||
aya-ebpf = { workspace = true }
|
||||
aya-log-ebpf = { workspace = true }
|
||||
network-types = "0.0.7"
|
||||
network-types = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
which = { workspace = true }
|
||||
@ -16,3 +15,6 @@ which = { workspace = true }
|
||||
[[bin]]
|
||||
name = "net-guardia-ingress"
|
||||
path = "src/main.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
bench = false
|
||||
5
ingress-ebpf/build.rs
Normal file
@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
if let Ok(linker) = which::which("bpf-linker") {
|
||||
println!("cargo:rerun-if-changed={}", linker.display());
|
||||
}
|
||||
}
|
||||
113
ingress-ebpf/src/action/access_control.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::HashMap;
|
||||
use aya_ebpf::maps::LpmTrie;
|
||||
use aya_ebpf::maps::lpm_trie::Key;
|
||||
use net_guardia_abi::define::setting::{MAX_GEO_ENTRIES, MAX_RULES};
|
||||
use net_guardia_abi::model::ip_address::{IPv4, IPv6};
|
||||
use net_guardia_abi::model::parsed_packet::ParsedPacket;
|
||||
use net_guardia_abi::model::port_rule::PortRule;
|
||||
|
||||
#[map]
|
||||
static IPV4_SRC_WHITELIST: HashMap<IPv4, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SRC_WHITELIST: HashMap<IPv6, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_DST_WHITELIST: HashMap<IPv4, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_DST_WHITELIST: HashMap<IPv6, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_SRC_BLACKLIST: HashMap<IPv4, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SRC_BLACKLIST: HashMap<IPv6, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_DST_BLACKLIST: HashMap<IPv4, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_DST_BLACKLIST: HashMap<IPv6, PortRule> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static GEO_BLOCK_V4: LpmTrie<u32, u8> = LpmTrie::with_max_entries(MAX_GEO_ENTRIES, 0);
|
||||
#[map]
|
||||
static GEO_BLOCK_V6: LpmTrie<u128, u8> = LpmTrie::with_max_entries(MAX_GEO_ENTRIES, 0);
|
||||
|
||||
pub fn ipv4_is_geo_blocked(pkt: &ParsedPacket) -> bool {
|
||||
let src_ip = u32::from_ne_bytes([pkt.src_ip[0], pkt.src_ip[1], pkt.src_ip[2], pkt.src_ip[3]]);
|
||||
let key = Key::new(32, src_ip);
|
||||
GEO_BLOCK_V4.get(&key).is_some()
|
||||
}
|
||||
|
||||
pub fn ipv6_is_geo_blocked(pkt: &ParsedPacket) -> bool {
|
||||
let src_ip = u128::from_ne_bytes(pkt.src_ip);
|
||||
let key = Key::new(128, src_ip);
|
||||
GEO_BLOCK_V6.get(&key).is_some()
|
||||
}
|
||||
|
||||
pub fn ipv4_is_whitelisted(pkt: &ParsedPacket) -> bool {
|
||||
let src_ip = pkt.src_ip_v4();
|
||||
let dst_ip = pkt.dst_ip_v4();
|
||||
unsafe {
|
||||
if let Some(rule) = IPV4_SRC_WHITELIST.get(&src_ip)
|
||||
&& rule.contains(pkt.src_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(rule) = IPV4_DST_WHITELIST.get(&dst_ip)
|
||||
&& rule.contains(pkt.dst_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn ipv6_is_whitelisted(pkt: &ParsedPacket) -> bool {
|
||||
let src_ip = pkt.src_ip_v6();
|
||||
let dst_ip = pkt.dst_ip_v6();
|
||||
unsafe {
|
||||
if let Some(rule) = IPV6_SRC_WHITELIST.get(&src_ip)
|
||||
&& rule.contains(pkt.src_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(rule) = IPV6_DST_WHITELIST.get(&dst_ip)
|
||||
&& rule.contains(pkt.dst_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn ipv4_is_blacklisted(pkt: &ParsedPacket) -> bool {
|
||||
let src_ip = pkt.src_ip_v4();
|
||||
let dst_ip = pkt.dst_ip_v4();
|
||||
unsafe {
|
||||
if let Some(rule) = IPV4_SRC_BLACKLIST.get(&src_ip)
|
||||
&& rule.contains(pkt.src_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(rule) = IPV4_DST_BLACKLIST.get(&dst_ip)
|
||||
&& rule.contains(pkt.dst_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn ipv6_is_blacklisted(pkt: &ParsedPacket) -> bool {
|
||||
let src_ip = pkt.src_ip_v6();
|
||||
let dst_ip = pkt.dst_ip_v6();
|
||||
unsafe {
|
||||
if let Some(rule) = IPV6_SRC_BLACKLIST.get(&src_ip)
|
||||
&& rule.contains(pkt.src_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(rule) = IPV6_DST_BLACKLIST.get(&dst_ip)
|
||||
&& rule.contains(pkt.dst_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
3
ingress-ebpf/src/action/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod access_control;
|
||||
pub mod protocol_filter;
|
||||
pub mod rate_limit;
|
||||
140
ingress-ebpf/src/action/protocol_filter.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use core::slice;
|
||||
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::{Array, HashMap};
|
||||
use net_guardia_abi::define::setting::MAX_RULES;
|
||||
use net_guardia_abi::define::tcp_flags::*;
|
||||
use net_guardia_abi::model::empty::EmptyMapValue;
|
||||
use net_guardia_abi::model::http_method::HttpMethodBitmap;
|
||||
use net_guardia_abi::model::ip_address::*;
|
||||
use net_guardia_abi::model::parsed_packet::ParsedPacket;
|
||||
use network_types::ip::IpProto;
|
||||
|
||||
#[map]
|
||||
static IPV4_HTTP_SERVICE: HashMap<AddrPortV4, HttpMethodBitmap> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_HTTP_SERVICE: HashMap<AddrPortV6, HttpMethodBitmap> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static SSH_WHITE_LIST_ENABLE: Array<EmptyMapValue> = Array::with_max_entries(1, 0);
|
||||
#[map]
|
||||
static IPV4_SSH_SERVICE: HashMap<AddrPortV4, EmptyMapValue> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SSH_SERVICE: HashMap<AddrPortV6, EmptyMapValue> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_SSH_WHITE_LIST: HashMap<IPv4, EmptyMapValue> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SSH_WHITE_LIST: HashMap<IPv6, EmptyMapValue> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_SSH_BLACK_LIST: HashMap<IPv4, EmptyMapValue> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SSH_BLACK_LIST: HashMap<IPv6, EmptyMapValue> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
|
||||
pub fn ipv4_service_rule_violation(start: usize, end: usize, pkt: &ParsedPacket) -> bool {
|
||||
let source = pkt.src_addr_v4();
|
||||
let destination = pkt.dst_addr_v4();
|
||||
http_service_violation(start, end, pkt, &IPV4_HTTP_SERVICE, &destination)
|
||||
|| ipv4_ssh_service_violation(&source, &destination)
|
||||
}
|
||||
|
||||
pub fn ipv6_service_rule_violation(start: usize, end: usize, pkt: &ParsedPacket) -> bool {
|
||||
let source = pkt.src_addr_v6();
|
||||
let destination = pkt.dst_addr_v6();
|
||||
http_service_violation(start, end, pkt, &IPV6_HTTP_SERVICE, &destination)
|
||||
|| ipv6_ssh_service_violation(&source, &destination)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn http_service_violation<K>(
|
||||
start: usize,
|
||||
end: usize,
|
||||
pkt: &ParsedPacket,
|
||||
map: &HashMap<K, HttpMethodBitmap>,
|
||||
destination: &K,
|
||||
) -> bool {
|
||||
match map.get_ptr_mut(destination) {
|
||||
Some(allow_method) => {
|
||||
if pkt.protocol != IpProto::Tcp as u8 {
|
||||
return false;
|
||||
}
|
||||
if pkt.tcp_flags & (TCP_SYN | TCP_RST | TCP_FIN) != 0 {
|
||||
return false;
|
||||
}
|
||||
if pkt.tcp_flags & (TCP_PSH | TCP_ACK) != (TCP_PSH | TCP_ACK) {
|
||||
return false;
|
||||
}
|
||||
if start + 15 > end {
|
||||
return false;
|
||||
}
|
||||
let l4_offset = match pkt.ip_version {
|
||||
value if value == IpVersion::V4.as_u8() => {
|
||||
14 + ((unsafe { *((start + 14) as *const u8) } & 0x0F) as usize) * 4
|
||||
}
|
||||
value if value == IpVersion::V6.as_u8() => 14 + 40,
|
||||
_ => return false,
|
||||
};
|
||||
if start + l4_offset + 13 > end {
|
||||
return false;
|
||||
}
|
||||
let doff = (unsafe { *((start + l4_offset + 12) as *const u8) } >> 4) as usize;
|
||||
if !(5..=15).contains(&doff) {
|
||||
return false;
|
||||
}
|
||||
let payload_offset = l4_offset + doff * 4;
|
||||
match get_http_request_method(start, end, payload_offset) {
|
||||
Some(http_method) => unsafe { *allow_method & http_method == 0 },
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_http_request_method(start: usize, end: usize, offset: usize) -> Option<HttpMethodBitmap> {
|
||||
if start + offset + 8 > end {
|
||||
return None;
|
||||
}
|
||||
let data = unsafe { slice::from_raw_parts((start + offset) as *const u8, 8) };
|
||||
match &data[..4] {
|
||||
b"GET " => Some(1 << 0),
|
||||
b"POST" if &data[4..5] == b" " => Some(1 << 1),
|
||||
b"PUT " => Some(1 << 2),
|
||||
b"DELE" if &data[4..7] == b"TE " => Some(1 << 3),
|
||||
b"HEAD" if &data[4..5] == b" " => Some(1 << 4),
|
||||
b"OPTI" if &data[4..8] == b"ONS " => Some(1 << 5),
|
||||
b"PATC" if &data[4..6] == b"H " => Some(1 << 6),
|
||||
b"TRAC" if &data[4..6] == b"E " => Some(1 << 7),
|
||||
b"CONN" if &data[4..8] == b"ECT " => Some(1 << 8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv4_ssh_service_violation(source: &AddrPortV4, destination: &AddrPortV4) -> bool {
|
||||
unsafe {
|
||||
if IPV4_SSH_SERVICE.get(destination).is_some() {
|
||||
if matches!(SSH_WHITE_LIST_ENABLE.get(0), Some(&v) if v != 0) {
|
||||
IPV4_SSH_WHITE_LIST.get(&source.ip()).is_none()
|
||||
} else {
|
||||
IPV4_SSH_BLACK_LIST.get(&source.ip()).is_some()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv6_ssh_service_violation(source: &AddrPortV6, destination: &AddrPortV6) -> bool {
|
||||
unsafe {
|
||||
if IPV6_SSH_SERVICE.get(destination).is_some() {
|
||||
if matches!(SSH_WHITE_LIST_ENABLE.get(0), Some(&v) if v != 0) {
|
||||
IPV6_SSH_WHITE_LIST.get(&source.ip()).is_none()
|
||||
} else {
|
||||
IPV6_SSH_BLACK_LIST.get(&source.ip()).is_some()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
188
ingress-ebpf/src/action/rate_limit.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use aya_ebpf::helpers::bpf_ktime_get_ns;
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::{Array, LruHashMap};
|
||||
use net_guardia_abi::define::drop_reason::*;
|
||||
use net_guardia_abi::define::rate_limit::*;
|
||||
use net_guardia_abi::define::setting::*;
|
||||
use net_guardia_abi::define::tcp_flags::*;
|
||||
use net_guardia_abi::model::ip_address::{IPv4, IPv6, IpVersion};
|
||||
use net_guardia_abi::model::parsed_packet::ParsedPacket;
|
||||
use net_guardia_abi::model::rate_limit::RateState;
|
||||
use network_types::ip::IpProto;
|
||||
|
||||
#[map]
|
||||
static RATE_LIMIT_CONFIG: Array<u64> = Array::with_max_entries(5, 0);
|
||||
#[map]
|
||||
static IPV4_PACKET_RATE_MAP: LruHashMap<IPv4, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
#[map]
|
||||
static IPV6_PACKET_RATE_MAP: LruHashMap<IPv6, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
#[map]
|
||||
static IPV4_SYN_RATE_MAP: LruHashMap<IPv4, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
#[map]
|
||||
static IPV6_SYN_RATE_MAP: LruHashMap<IPv6, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
#[map]
|
||||
static IPV4_UDP_RATE_MAP: LruHashMap<IPv4, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
#[map]
|
||||
static IPV6_UDP_RATE_MAP: LruHashMap<IPv6, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
#[map]
|
||||
static IPV4_DNS_RATE_MAP: LruHashMap<IPv4, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
#[map]
|
||||
static IPV6_DNS_RATE_MAP: LruHashMap<IPv6, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
|
||||
pub fn should_drop(pkt: &ParsedPacket) -> Option<u8> {
|
||||
match pkt.ip_version {
|
||||
value if value == IpVersion::V4.as_u8() => ipv4_should_drop(pkt),
|
||||
value if value == IpVersion::V6.as_u8() => ipv6_should_drop(pkt),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_config(index: u32, default: u64) -> u64 {
|
||||
RATE_LIMIT_CONFIG
|
||||
.get(index)
|
||||
.copied()
|
||||
.filter(|&v| v > 0)
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_syn_only(pkt: &ParsedPacket) -> bool {
|
||||
pkt.protocol == IpProto::Tcp as u8 && (pkt.tcp_flags & TCP_SYN != 0) && (pkt.tcp_flags & TCP_ACK == 0)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn check_rate<K>(map: &LruHashMap<K, RateState>, key: &K, now: u64, window: u64, limit: u64) -> bool {
|
||||
unsafe {
|
||||
if let Some(state) = map.get_ptr_mut(key) {
|
||||
if now - (*state).window_start >= window {
|
||||
(*state).count = 1;
|
||||
(*state).window_start = now;
|
||||
} else {
|
||||
(*state).count += 1;
|
||||
if (*state).count > limit {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let new_state = RateState {
|
||||
count: 1,
|
||||
window_start: now,
|
||||
};
|
||||
let _ = map.insert(key, &new_state, 0);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv4_should_drop(pkt: &ParsedPacket) -> Option<u8> {
|
||||
let now = unsafe { bpf_ktime_get_ns() };
|
||||
let window = get_config(CFG_WINDOW_NS, DEFAULT_WINDOW_NS);
|
||||
let src_ip = pkt.src_ip_v4();
|
||||
|
||||
if check_rate(
|
||||
&IPV4_PACKET_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_PACKET_RATE, DEFAULT_PACKET_RATE),
|
||||
) {
|
||||
return Some(DROP_REASON_RATE_LIMIT_PKT);
|
||||
}
|
||||
|
||||
if is_syn_only(pkt)
|
||||
&& check_rate(
|
||||
&IPV4_SYN_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_SYN_RATE, DEFAULT_SYN_RATE),
|
||||
)
|
||||
{
|
||||
return Some(DROP_REASON_RATE_LIMIT_SYN);
|
||||
}
|
||||
|
||||
if pkt.protocol == IpProto::Udp as u8
|
||||
&& check_rate(
|
||||
&IPV4_UDP_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_UDP_RATE, DEFAULT_UDP_RATE),
|
||||
)
|
||||
{
|
||||
return Some(DROP_REASON_RATE_LIMIT_UDP);
|
||||
}
|
||||
|
||||
if pkt.protocol == IpProto::Udp as u8
|
||||
&& pkt.dst_port == 53
|
||||
&& check_rate(
|
||||
&IPV4_DNS_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_DNS_RATE, DEFAULT_DNS_RATE),
|
||||
)
|
||||
{
|
||||
return Some(DROP_REASON_RATE_LIMIT_DNS);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv6_should_drop(pkt: &ParsedPacket) -> Option<u8> {
|
||||
let now = unsafe { bpf_ktime_get_ns() };
|
||||
let window = get_config(CFG_WINDOW_NS, DEFAULT_WINDOW_NS);
|
||||
let src_ip = pkt.src_ip_v6();
|
||||
|
||||
if check_rate(
|
||||
&IPV6_PACKET_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_PACKET_RATE, DEFAULT_PACKET_RATE),
|
||||
) {
|
||||
return Some(DROP_REASON_RATE_LIMIT_PKT);
|
||||
}
|
||||
|
||||
if is_syn_only(pkt)
|
||||
&& check_rate(
|
||||
&IPV6_SYN_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_SYN_RATE, DEFAULT_SYN_RATE),
|
||||
)
|
||||
{
|
||||
return Some(DROP_REASON_RATE_LIMIT_SYN);
|
||||
}
|
||||
|
||||
if pkt.protocol == IpProto::Udp as u8
|
||||
&& check_rate(
|
||||
&IPV6_UDP_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_UDP_RATE, DEFAULT_UDP_RATE),
|
||||
)
|
||||
{
|
||||
return Some(DROP_REASON_RATE_LIMIT_UDP);
|
||||
}
|
||||
|
||||
if pkt.protocol == IpProto::Udp as u8
|
||||
&& pkt.dst_port == 53
|
||||
&& check_rate(
|
||||
&IPV6_DNS_RATE_MAP,
|
||||
&src_ip,
|
||||
now,
|
||||
window,
|
||||
get_config(CFG_DNS_RATE, DEFAULT_DNS_RATE),
|
||||
)
|
||||
{
|
||||
return Some(DROP_REASON_RATE_LIMIT_DNS);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
1
ingress-ebpf/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
#![no_std]
|
||||
232
ingress-ebpf/src/main.rs
Normal file
@ -0,0 +1,232 @@
|
||||
#![cfg_attr(any(target_arch = "bpf", target_os = "none"), no_std)]
|
||||
#![no_main]
|
||||
mod action;
|
||||
|
||||
use aya_ebpf::bindings::xdp_action;
|
||||
use aya_ebpf::helpers::bpf_ktime_get_ns;
|
||||
use aya_ebpf::macros::{map, xdp};
|
||||
use aya_ebpf::maps::{Array, PerCpuArray, ProgramArray, RingBuf, XskMap};
|
||||
use aya_ebpf::programs::XdpContext;
|
||||
#[allow(unused_imports)]
|
||||
use aya_log_ebpf::info;
|
||||
use net_guardia_abi::define::drop_reason::*;
|
||||
use net_guardia_abi::define::pipeline::*;
|
||||
use net_guardia_abi::ebpf::parsing;
|
||||
use net_guardia_abi::ebpf::symmetric_hash::symmetric_queue_id;
|
||||
use net_guardia_abi::model::drop_event::DropEvent;
|
||||
use net_guardia_abi::model::ip_address::IpVersion;
|
||||
use net_guardia_abi::model::parsed_packet::ParsedPacket;
|
||||
|
||||
use crate::action::{access_control, protocol_filter, rate_limit};
|
||||
|
||||
#[map]
|
||||
static PROGRAM_ARRAY: ProgramArray = ProgramArray::with_max_entries(MAX_STAGES, 0);
|
||||
#[map]
|
||||
static NEXT_STAGE: Array<u32> = Array::with_max_entries(MAX_STAGES, 0);
|
||||
#[map]
|
||||
static PARSED_PACKET: PerCpuArray<ParsedPacket> = PerCpuArray::with_max_entries(1, 0);
|
||||
#[map]
|
||||
static INGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0);
|
||||
#[map]
|
||||
static NUM_QUEUES: Array<u32> = Array::with_max_entries(1, 0);
|
||||
#[map]
|
||||
static DROP_EVENTS: RingBuf = RingBuf::with_byte_size(256 * 1024, 0);
|
||||
|
||||
#[xdp]
|
||||
pub fn net_guardia(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
packet_intake(&ctx);
|
||||
let _ = PROGRAM_ARRAY.tail_call(&ctx, STAGE_TRANSMISSION);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn chain_next(ctx: &XdpContext, current_id: u32) {
|
||||
unsafe {
|
||||
if let Some(&next_slot) = NEXT_STAGE.get(current_id)
|
||||
&& next_slot != STAGE_NONE
|
||||
{
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, next_slot);
|
||||
}
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, STAGE_TRANSMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn emit_drop_event(pkt: &ParsedPacket, reason: u8) {
|
||||
if let Some(mut entry) = DROP_EVENTS.reserve::<DropEvent>(0) {
|
||||
unsafe {
|
||||
let event = &mut *entry.as_mut_ptr();
|
||||
event.timestamp_ns = bpf_ktime_get_ns();
|
||||
event.src_ip = pkt.src_ip;
|
||||
event.dst_ip = pkt.dst_ip;
|
||||
event.src_port = pkt.src_port;
|
||||
event.dst_port = pkt.dst_port;
|
||||
event.protocol = pkt.protocol;
|
||||
event.reason = reason;
|
||||
event.ip_version = pkt.ip_version;
|
||||
event._pad = 0;
|
||||
}
|
||||
entry.submit(0);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn packet_intake(ctx: &XdpContext) {
|
||||
let Some(ptr) = PARSED_PACKET.get_ptr_mut(0) else {
|
||||
return;
|
||||
};
|
||||
if unsafe { parsing::parse_packet(ctx.data(), ctx.data_end(), ptr).is_some() } {
|
||||
unsafe {
|
||||
(*ptr).timestamp_ns = bpf_ktime_get_ns();
|
||||
}
|
||||
chain_next(ctx, STAGE_ENTRY);
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn access_control(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
match try_access_control(&ctx) {
|
||||
Ok(action) => action,
|
||||
Err(_) => {
|
||||
chain_next(&ctx, STAGE_ACCESS_CONTROL);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn try_access_control(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let pkt = &*ptr;
|
||||
match pkt.ip_version {
|
||||
value if value == IpVersion::V4.as_u8() => {
|
||||
if access_control::ipv4_is_whitelisted(pkt) {
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, STAGE_TRANSMISSION);
|
||||
return Err(());
|
||||
}
|
||||
if access_control::ipv4_is_geo_blocked(pkt) {
|
||||
emit_drop_event(pkt, DROP_REASON_GEO_BLOCK);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
if access_control::ipv4_is_blacklisted(pkt) {
|
||||
emit_drop_event(pkt, DROP_REASON_ACL_BLACKLIST);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
value if value == IpVersion::V6.as_u8() => {
|
||||
if access_control::ipv6_is_whitelisted(pkt) {
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, STAGE_TRANSMISSION);
|
||||
return Err(());
|
||||
}
|
||||
if access_control::ipv6_is_geo_blocked(pkt) {
|
||||
emit_drop_event(pkt, DROP_REASON_GEO_BLOCK);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
if access_control::ipv6_is_blacklisted(pkt) {
|
||||
emit_drop_event(pkt, DROP_REASON_ACL_BLACKLIST);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
chain_next(ctx, STAGE_ACCESS_CONTROL);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn rate_limit(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
match try_rate_limit(&ctx) {
|
||||
Ok(action) => action,
|
||||
Err(_) => {
|
||||
chain_next(&ctx, STAGE_RATE_LIMIT);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn try_rate_limit(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let pkt = &*ptr;
|
||||
if let Some(reason) = rate_limit::should_drop(pkt) {
|
||||
emit_drop_event(pkt, reason);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
chain_next(ctx, STAGE_RATE_LIMIT);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn protocol_filter(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
match try_protocol_filter(&ctx) {
|
||||
Ok(action) => action,
|
||||
Err(_) => {
|
||||
chain_next(&ctx, STAGE_SERVICE);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn try_protocol_filter(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let start = ctx.data();
|
||||
let end = ctx.data_end();
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let pkt = &*ptr;
|
||||
|
||||
match pkt.ip_version {
|
||||
value if value == IpVersion::V4.as_u8() => {
|
||||
if protocol_filter::ipv4_service_rule_violation(start, end, pkt) {
|
||||
emit_drop_event(pkt, DROP_REASON_PROTOCOL_FILTER);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
value
|
||||
if value == IpVersion::V6.as_u8() && protocol_filter::ipv6_service_rule_violation(start, end, pkt) =>
|
||||
{
|
||||
emit_drop_event(pkt, DROP_REASON_PROTOCOL_FILTER);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
chain_next(ctx, STAGE_SERVICE);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn compute_symmetric_queue_id() -> Option<u32> {
|
||||
unsafe {
|
||||
let pkt = &*PARSED_PACKET.get_ptr(0)?;
|
||||
let num_q = *NUM_QUEUES.get(0)?;
|
||||
symmetric_queue_id(pkt, num_q)
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn transmission(ctx: XdpContext) -> u32 {
|
||||
let queue_id = unsafe { compute_symmetric_queue_id().unwrap_or((*ctx.ctx).rx_queue_index) };
|
||||
match INGRESS_XSKS_MAP.redirect(queue_id, 0) {
|
||||
Ok(action) => action,
|
||||
Err(_) => xdp_action::XDP_PASS,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(test), any(target_arch = "bpf", target_os = "none")))]
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
unsafe { core::hint::unreachable_unchecked() }
|
||||
}
|
||||
12
macros/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "macros"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
591
macros/src/config.rs
Normal file
@ -0,0 +1,591 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Error, Fields, Ident, ItemStruct, LitBool, LitStr, Result, Token, Type};
|
||||
|
||||
struct StructAttr {
|
||||
default_section: Option<String>,
|
||||
}
|
||||
|
||||
impl Parse for StructAttr {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut section = None;
|
||||
while !input.is_empty() {
|
||||
let key: Ident = input.parse()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
let val: LitStr = input.parse()?;
|
||||
if key == "section" {
|
||||
section = Some(val.value());
|
||||
}
|
||||
if !input.is_empty() {
|
||||
input.parse::<Token![,]>()?;
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
default_section: section,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum ConfigField {
|
||||
Setting(SettingField),
|
||||
Flatten(FlattenField),
|
||||
MappedParent(MappedParent),
|
||||
}
|
||||
|
||||
struct SettingField {
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
key: String,
|
||||
default: String,
|
||||
default_debug: Option<String>,
|
||||
section: Option<String>,
|
||||
api: bool,
|
||||
}
|
||||
|
||||
struct FlattenField {
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
struct MappedSetting {
|
||||
key: String,
|
||||
default: String,
|
||||
default_debug: Option<String>,
|
||||
parent: String,
|
||||
sub_field: String,
|
||||
section: Option<String>,
|
||||
api: bool,
|
||||
}
|
||||
|
||||
struct MappedParent {
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
settings: Vec<MappedSetting>,
|
||||
}
|
||||
|
||||
fn parse_struct_mapped_settings(
|
||||
input: &mut ItemStruct,
|
||||
default_section: &Option<String>,
|
||||
) -> Result<Vec<MappedSetting>> {
|
||||
let mut mapped = Vec::new();
|
||||
let mut retained = Vec::new();
|
||||
for attr in input.attrs.drain(..) {
|
||||
if !attr.path().is_ident("setting") {
|
||||
retained.push(attr);
|
||||
continue;
|
||||
}
|
||||
let mut key = None;
|
||||
let mut default = None;
|
||||
let mut default_debug = None;
|
||||
let mut path = None;
|
||||
let mut section = None;
|
||||
let mut api = true;
|
||||
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("key") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
key = Some(val.value());
|
||||
} else if meta.path.is_ident("default") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default = Some(val.value());
|
||||
} else if meta.path.is_ident("default_debug") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default_debug = Some(val.value());
|
||||
} else if meta.path.is_ident("path") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
path = Some(val.value());
|
||||
} else if meta.path.is_ident("section") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
section = Some(val.value());
|
||||
} else if meta.path.is_ident("api") {
|
||||
let val: LitBool = meta.value()?.parse()?;
|
||||
api = val.value();
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
if let (Some(key), Some(default), Some(path)) = (key, default, path) {
|
||||
let (parent, sub_field) = path
|
||||
.split_once('.')
|
||||
.ok_or_else(|| Error::new(attr.span(), "#[setting] `path` must be `parent.sub_field`"))?;
|
||||
mapped.push(MappedSetting {
|
||||
key,
|
||||
default,
|
||||
default_debug,
|
||||
parent: parent.to_string(),
|
||||
sub_field: sub_field.to_string(),
|
||||
section: section.or_else(|| default_section.clone()),
|
||||
api,
|
||||
});
|
||||
} else {
|
||||
retained.push(attr);
|
||||
}
|
||||
}
|
||||
input.attrs = retained;
|
||||
Ok(mapped)
|
||||
}
|
||||
|
||||
fn parse_field(field: &mut syn::Field, default_section: &Option<String>) -> Result<Option<ConfigField>> {
|
||||
let Some(idx) = field.attrs.iter().position(|a| a.path().is_ident("setting")) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let attr = field.attrs.remove(idx);
|
||||
|
||||
let mut is_flatten = false;
|
||||
let mut key = None;
|
||||
let mut default = None;
|
||||
let mut default_debug = None;
|
||||
let mut section = None;
|
||||
let mut api = true;
|
||||
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("flatten") {
|
||||
is_flatten = true;
|
||||
} else if meta.path.is_ident("key") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
key = Some(val.value());
|
||||
} else if meta.path.is_ident("default") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default = Some(val.value());
|
||||
} else if meta.path.is_ident("default_debug") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default_debug = Some(val.value());
|
||||
} else if meta.path.is_ident("section") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
section = Some(val.value());
|
||||
} else if meta.path.is_ident("api") {
|
||||
let val: LitBool = meta.value()?.parse()?;
|
||||
api = val.value();
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let ident = field
|
||||
.ident
|
||||
.clone()
|
||||
.ok_or_else(|| Error::new(field.span(), "#[setting] only supports named fields"))?;
|
||||
let ty = field.ty.clone();
|
||||
|
||||
if is_flatten {
|
||||
return Ok(Some(ConfigField::Flatten(FlattenField { ident, ty })));
|
||||
}
|
||||
|
||||
let key = key.ok_or_else(|| Error::new(attr.span(), "#[setting] requires `key`"))?;
|
||||
let default = default.ok_or_else(|| Error::new(attr.span(), "#[setting] requires `default`"))?;
|
||||
|
||||
Ok(Some(ConfigField::Setting(SettingField {
|
||||
ident,
|
||||
ty,
|
||||
key,
|
||||
default,
|
||||
default_debug,
|
||||
section: section.or_else(|| default_section.clone()),
|
||||
api,
|
||||
})))
|
||||
}
|
||||
|
||||
fn is_type(ty: &Type, name: &str) -> bool {
|
||||
matches!(ty, Type::Path(tp) if tp.path.is_ident(name))
|
||||
}
|
||||
|
||||
fn is_vec_string(ty: &Type) -> bool {
|
||||
if let Type::Path(tp) = ty
|
||||
&& let Some(seg) = tp.path.segments.last()
|
||||
{
|
||||
return seg.ident == "Vec";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn parse_default_tokens(ty: &Type, default: &str) -> Result<TokenStream2> {
|
||||
default.parse::<TokenStream2>().map_err(|err| {
|
||||
Error::new(
|
||||
ty.span(),
|
||||
format!("invalid #[setting] default literal `{default}`: {err}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn make_default_val(ty: &Type, default: &str) -> Result<TokenStream2> {
|
||||
if is_type(ty, "String") {
|
||||
Ok(quote! { #default.to_string() })
|
||||
} else if is_type(ty, "bool") {
|
||||
let val = default == "true" || default == "1";
|
||||
Ok(quote! { #val })
|
||||
} else if is_vec_string(ty) {
|
||||
if default.is_empty() {
|
||||
Ok(quote! { Vec::new() })
|
||||
} else {
|
||||
let items: Vec<&str> = default.split(',').map(|v| v.trim()).collect();
|
||||
Ok(quote! { vec![#(#items.to_string()),*] })
|
||||
}
|
||||
} else {
|
||||
let value = parse_default_tokens(ty, default)?;
|
||||
Ok(quote! { #value })
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_default(f: &SettingField) -> Result<TokenStream2> {
|
||||
let ident = &f.ident;
|
||||
let ty = &f.ty;
|
||||
|
||||
match &f.default_debug {
|
||||
Some(dbg) => {
|
||||
let release_val = make_default_val(ty, &f.default)?;
|
||||
let debug_val = make_default_val(ty, dbg)?;
|
||||
Ok(quote! { #ident: if cfg!(debug_assertions) { #debug_val } else { #release_val } })
|
||||
}
|
||||
None => {
|
||||
let val = make_default_val(ty, &f.default)?;
|
||||
Ok(quote! { #ident: #val })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_flatten_default(f: &FlattenField) -> TokenStream2 {
|
||||
let ident = &f.ident;
|
||||
let ty = &f.ty;
|
||||
quote! { #ident: #ty::defaults() }
|
||||
}
|
||||
|
||||
fn gen_mapped_default(mp: &MappedParent) -> Result<TokenStream2> {
|
||||
let ident = &mp.ident;
|
||||
let ty = &mp.ty;
|
||||
let sub_fields: Vec<_> = mp
|
||||
.settings
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let sub = format_ident!("{}", s.sub_field);
|
||||
let val: TokenStream2 = match &s.default_debug {
|
||||
Some(dbg) => {
|
||||
let debug = parse_default_tokens(&mp.ty, dbg)?;
|
||||
let release = parse_default_tokens(&mp.ty, &s.default)?;
|
||||
quote! { if cfg!(debug_assertions) { #debug } else { #release } }
|
||||
}
|
||||
None => parse_default_tokens(&mp.ty, &s.default)?,
|
||||
};
|
||||
Ok(quote! { #sub: #val })
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(quote! { #ident: #ty { #(#sub_fields,)* } })
|
||||
}
|
||||
|
||||
fn gen_apply_value(f: &SettingField) -> TokenStream2 {
|
||||
let ident = &f.ident;
|
||||
let key = &f.key;
|
||||
let ty = &f.ty;
|
||||
|
||||
if is_type(ty, "String") {
|
||||
quote! {
|
||||
if let Some(v) = values.get(#key)
|
||||
&& !v.is_empty()
|
||||
{
|
||||
self.#ident = v.clone();
|
||||
}
|
||||
}
|
||||
} else if is_type(ty, "bool") {
|
||||
quote! {
|
||||
if let Some(v) = values.get(#key) {
|
||||
self.#ident = v == "true" || v == "1";
|
||||
}
|
||||
}
|
||||
} else if is_vec_string(ty) {
|
||||
quote! {
|
||||
if let Some(v) = values.get(#key) {
|
||||
self.#ident = if v.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
v.split(',').map(|s| s.trim().to_string()).collect()
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
if let Some(v) = values.get(#key)
|
||||
&& let Ok(parsed) = v.parse()
|
||||
{
|
||||
self.#ident = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_flatten_apply(f: &FlattenField) -> TokenStream2 {
|
||||
let ident = &f.ident;
|
||||
quote! { self.#ident.apply_config_values(values); }
|
||||
}
|
||||
|
||||
fn gen_mapped_apply(mp: &MappedParent) -> TokenStream2 {
|
||||
let parent = &mp.ident;
|
||||
let calls: Vec<_> = mp
|
||||
.settings
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let sub = format_ident!("{}", s.sub_field);
|
||||
let key = &s.key;
|
||||
quote! {
|
||||
if let Some(v) = values.get(#key)
|
||||
&& let Ok(parsed) = v.parse()
|
||||
{
|
||||
self.#parent.#sub = parsed;
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
quote! { #(#calls)* }
|
||||
}
|
||||
|
||||
fn gen_default_setting(f: &SettingField) -> TokenStream2 {
|
||||
let key = &f.key;
|
||||
let default = &f.default;
|
||||
|
||||
match &f.default_debug {
|
||||
Some(dbg) => quote! {
|
||||
settings.push((#key, if cfg!(debug_assertions) { #dbg.to_string() } else { #default.to_string() }));
|
||||
},
|
||||
None => quote! {
|
||||
settings.push((#key, #default.to_string()));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_flatten_default_settings(f: &FlattenField) -> TokenStream2 {
|
||||
let ty = &f.ty;
|
||||
quote! { settings.extend(#ty::default_settings()); }
|
||||
}
|
||||
|
||||
fn gen_mapped_default_settings(mp: &MappedParent) -> TokenStream2 {
|
||||
let calls: Vec<_> = mp
|
||||
.settings
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let key = &s.key;
|
||||
let default = &s.default;
|
||||
match &s.default_debug {
|
||||
Some(dbg) => quote! {
|
||||
settings.push((#key, if cfg!(debug_assertions) { #dbg.to_string() } else { #default.to_string() }));
|
||||
},
|
||||
None => quote! {
|
||||
settings.push((#key, #default.to_string()));
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
quote! { #(#calls)* }
|
||||
}
|
||||
|
||||
fn collect_api_keys(fields: &[ConfigField]) -> Vec<(&str, &str)> {
|
||||
let mut keys = Vec::new();
|
||||
for f in fields {
|
||||
match f {
|
||||
ConfigField::Setting(s) if s.api => {
|
||||
let sec = s.section.as_deref().unwrap_or("default");
|
||||
keys.push((sec, s.key.as_str()));
|
||||
}
|
||||
ConfigField::MappedParent(mp) => {
|
||||
for s in &mp.settings {
|
||||
if s.api {
|
||||
let sec = s.section.as_deref().unwrap_or("default");
|
||||
keys.push((sec, s.key.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn gen_keys_consts(fields: &[ConfigField]) -> TokenStream2 {
|
||||
let api_keys = collect_api_keys(fields);
|
||||
if api_keys.is_empty() {
|
||||
return quote! {};
|
||||
}
|
||||
|
||||
let mut sections: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
||||
for (sec, key) in &api_keys {
|
||||
sections.entry(sec).or_default().push(key);
|
||||
}
|
||||
|
||||
let single = sections.len() == 1;
|
||||
sections
|
||||
.iter()
|
||||
.map(|(section, keys)| {
|
||||
let name = if single {
|
||||
format_ident!("API_KEYS")
|
||||
} else {
|
||||
format_ident!("{}_KEYS", section.to_uppercase())
|
||||
};
|
||||
quote! { pub const #name: &[&str] = &[#(#keys),*]; }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn value_to_string_expr(ty: &Type, expr: TokenStream2) -> TokenStream2 {
|
||||
if is_type(ty, "String") {
|
||||
quote! { #expr.clone() }
|
||||
} else if is_vec_string(ty) {
|
||||
quote! { #expr.join(",") }
|
||||
} else {
|
||||
quote! { #expr.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_api_values(fields: &[ConfigField]) -> TokenStream2 {
|
||||
let entries: Vec<_> = fields
|
||||
.iter()
|
||||
.flat_map(|f| match f {
|
||||
ConfigField::Setting(s) if s.api => {
|
||||
let key = &s.key;
|
||||
let ident = &s.ident;
|
||||
let value = value_to_string_expr(&s.ty, quote! { self.#ident });
|
||||
vec![quote! { values.push((#key, #value)); }]
|
||||
}
|
||||
ConfigField::Flatten(f) => {
|
||||
let ident = &f.ident;
|
||||
vec![quote! { values.extend(self.#ident.api_values()); }]
|
||||
}
|
||||
ConfigField::MappedParent(mp) => mp
|
||||
.settings
|
||||
.iter()
|
||||
.filter(|s| s.api)
|
||||
.map(|s| {
|
||||
let key = &s.key;
|
||||
let parent = &mp.ident;
|
||||
let sub = format_ident!("{}", s.sub_field);
|
||||
quote! { values.push((#key, self.#parent.#sub.to_string())); }
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
quote! {
|
||||
pub fn api_values(&self) -> Vec<(&'static str, String)> {
|
||||
let mut values = Vec::new();
|
||||
#(#entries)*
|
||||
values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_settings_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let struct_attr = syn::parse_macro_input!(attr as StructAttr);
|
||||
let mut input = syn::parse_macro_input!(item as ItemStruct);
|
||||
|
||||
let mapped_settings = match parse_struct_mapped_settings(&mut input, &struct_attr.default_section) {
|
||||
Ok(settings) => settings,
|
||||
Err(err) => return err.to_compile_error().into(),
|
||||
};
|
||||
|
||||
let mut mapped_groups: BTreeMap<String, Vec<MappedSetting>> = BTreeMap::new();
|
||||
for ms in mapped_settings {
|
||||
mapped_groups.entry(ms.parent.clone()).or_default().push(ms);
|
||||
}
|
||||
|
||||
let fields = match &mut input.fields {
|
||||
Fields::Named(f) => f,
|
||||
_ => {
|
||||
return Error::new(input.span(), "config_settings only supports named fields")
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
};
|
||||
|
||||
let mut config_fields = Vec::new();
|
||||
for field in &mut fields.named {
|
||||
let Some(field_ident) = field.ident.clone() else {
|
||||
return Error::new(field.span(), "config_settings only supports named fields")
|
||||
.to_compile_error()
|
||||
.into();
|
||||
};
|
||||
let field_name = field_ident.to_string();
|
||||
|
||||
if let Some(settings) = mapped_groups.remove(&field_name) {
|
||||
config_fields.push(ConfigField::MappedParent(MappedParent {
|
||||
ident: field_ident,
|
||||
ty: field.ty.clone(),
|
||||
settings,
|
||||
}));
|
||||
} else {
|
||||
match parse_field(field, &struct_attr.default_section) {
|
||||
Ok(Some(cf)) => config_fields.push(cf),
|
||||
Ok(None) => {}
|
||||
Err(err) => return err.to_compile_error().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let struct_name = &input.ident;
|
||||
let keys_consts = gen_keys_consts(&config_fields);
|
||||
let api_values = gen_api_values(&config_fields);
|
||||
|
||||
let default_fields: Vec<_> = match config_fields
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
ConfigField::Setting(s) => gen_default(s),
|
||||
ConfigField::Flatten(s) => Ok(gen_flatten_default(s)),
|
||||
ConfigField::MappedParent(mp) => gen_mapped_default(mp),
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
{
|
||||
Ok(fields) => fields,
|
||||
Err(err) => return err.to_compile_error().into(),
|
||||
};
|
||||
|
||||
let apply_calls: Vec<_> = config_fields
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
ConfigField::Setting(s) => gen_apply_value(s),
|
||||
ConfigField::Flatten(s) => gen_flatten_apply(s),
|
||||
ConfigField::MappedParent(mp) => gen_mapped_apply(mp),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let default_setting_calls: Vec<_> = config_fields
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
ConfigField::Setting(s) => gen_default_setting(s),
|
||||
ConfigField::Flatten(s) => gen_flatten_default_settings(s),
|
||||
ConfigField::MappedParent(mp) => gen_mapped_default_settings(mp),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let expanded = quote! {
|
||||
#input
|
||||
|
||||
impl #struct_name {
|
||||
#keys_consts
|
||||
#api_values
|
||||
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
#(#default_fields,)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_config_values(values: &super::ConfigValues) -> Self {
|
||||
let mut cfg = Self::defaults();
|
||||
cfg.apply_config_values(values);
|
||||
cfg
|
||||
}
|
||||
|
||||
pub fn apply_config_values(&mut self, values: &super::ConfigValues) {
|
||||
#(#apply_calls)*
|
||||
}
|
||||
|
||||
pub fn default_settings() -> Vec<(&'static str, String)> {
|
||||
let mut settings = Vec::new();
|
||||
#(#default_setting_calls)*
|
||||
settings
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
220
macros/src/error_enum.rs
Normal file
@ -0,0 +1,220 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Attribute, Error, Expr, Ident, LitStr, Result, Token, Type};
|
||||
|
||||
pub struct ErrorVariant {
|
||||
pub attributes: Vec<Attribute>,
|
||||
pub error_msg: LitStr,
|
||||
pub name: Ident,
|
||||
pub fields: Vec<(Ident, Type)>,
|
||||
pub level: Expr,
|
||||
}
|
||||
|
||||
impl ErrorVariant {
|
||||
pub fn has_no_source(&self) -> bool {
|
||||
self.attributes.iter().any(|attr| attr.path().is_ident("no_source"))
|
||||
}
|
||||
|
||||
pub fn should_generate_constructor(&self, force_no_source: bool) -> bool {
|
||||
if force_no_source || self.has_no_source() {
|
||||
!self.fields.is_empty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ErrorEnumInput {
|
||||
pub enum_name: Ident,
|
||||
pub variants: Vec<ErrorVariant>,
|
||||
}
|
||||
|
||||
impl Parse for ErrorEnumInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let enum_name = input.parse::<Ident>()?;
|
||||
|
||||
let content;
|
||||
syn::braced!(content in input);
|
||||
|
||||
let mut variants = Vec::new();
|
||||
|
||||
while !content.is_empty() {
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
while content.peek(Token![#]) {
|
||||
attributes.push(content.call(Attribute::parse_outer)?);
|
||||
}
|
||||
|
||||
let attributes: Vec<_> = attributes.into_iter().flatten().collect();
|
||||
|
||||
let error_attr = attributes
|
||||
.iter()
|
||||
.find(|attr| attr.path().is_ident("error"))
|
||||
.ok_or_else(|| Error::new(content.span(), "Missing #[error] attribute"))?;
|
||||
|
||||
let error_msg = match &error_attr.meta {
|
||||
syn::Meta::List(list) => syn::parse2::<LitStr>(list.tokens.clone())?,
|
||||
_ => {
|
||||
return Err(Error::new(error_attr.span(), "Invalid error attribute format"));
|
||||
}
|
||||
};
|
||||
|
||||
let name = content.parse::<Ident>()?;
|
||||
|
||||
let mut fields = Vec::new();
|
||||
if content.peek(syn::token::Brace) {
|
||||
let fields_content;
|
||||
syn::braced!(fields_content in content);
|
||||
|
||||
while !fields_content.is_empty() {
|
||||
let field_name = fields_content.parse::<Ident>()?;
|
||||
fields_content.parse::<Token![:]>()?;
|
||||
let field_type = fields_content.parse::<Type>()?;
|
||||
fields.push((field_name, field_type));
|
||||
|
||||
if !fields_content.is_empty() {
|
||||
fields_content.parse::<Token![,]>()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content.parse::<Token![=>]>()?;
|
||||
let level = content.parse::<Expr>()?;
|
||||
|
||||
if !content.is_empty() {
|
||||
content.parse::<Token![,]>()?;
|
||||
}
|
||||
|
||||
variants.push(ErrorVariant {
|
||||
attributes,
|
||||
error_msg,
|
||||
name,
|
||||
fields,
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ErrorEnumInput { enum_name, variants })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_error_enum(input: TokenStream, force_no_source: bool) -> TokenStream {
|
||||
let input = syn::parse_macro_input!(input as ErrorEnumInput);
|
||||
|
||||
let enum_name = &input.enum_name;
|
||||
let variants = &input.variants;
|
||||
|
||||
let enum_variants = variants.iter().map(|variant| {
|
||||
let name = &variant.name;
|
||||
let error_msg = &variant.error_msg;
|
||||
let fields = &variant.fields;
|
||||
|
||||
let field_definitions = fields.iter().map(|(name, ty)| {
|
||||
quote! { #name: #ty }
|
||||
});
|
||||
|
||||
if force_no_source || variant.has_no_source() {
|
||||
if variant.fields.is_empty() {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name { #(#field_definitions,)* }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name {
|
||||
#(#field_definitions,)*
|
||||
err: String
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let level_match_arms = variants.iter().map(|variant| {
|
||||
let name = &variant.name;
|
||||
let level = &variant.level;
|
||||
|
||||
if force_no_source || variant.has_no_source() {
|
||||
if variant.fields.is_empty() {
|
||||
quote! {
|
||||
Self::#name => #level
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Self::#name { .. } => #level
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Self::#name { err: _, .. } => #level
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let constructors = variants.iter().filter_map(|variant| {
|
||||
if !variant.should_generate_constructor(force_no_source) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = &variant.name;
|
||||
let fields = &variant.fields;
|
||||
|
||||
let params = fields.iter().map(|(field_name, field_type)| {
|
||||
quote! { #field_name: impl Into<#field_type> }
|
||||
});
|
||||
|
||||
let field_assignments = fields.iter().map(|(field_name, _)| {
|
||||
quote! { #field_name: #field_name.into() }
|
||||
});
|
||||
|
||||
if force_no_source || variant.has_no_source() {
|
||||
Some(quote! {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #name(#(#params),*) -> Self {
|
||||
Self::#name {
|
||||
#(#field_assignments,)*
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(quote! {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #name(#(#params,)* source: impl std::fmt::Display) -> Self {
|
||||
Self::#name {
|
||||
#(#field_assignments,)*
|
||||
err: source.to_string()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let expanded = quote! {
|
||||
#[allow(dead_code, clippy::enum_variant_names)]
|
||||
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
|
||||
pub enum #enum_name {
|
||||
#(#enum_variants,)*
|
||||
}
|
||||
|
||||
impl #enum_name {
|
||||
#[allow(dead_code)]
|
||||
pub fn level(&self) -> tracing::Level {
|
||||
match self {
|
||||
#(#level_match_arms,)*
|
||||
}
|
||||
}
|
||||
|
||||
#(#constructors)*
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
187
macros/src/fallible.rs
Normal file
@ -0,0 +1,187 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Attribute, Error, Ident, LitStr, Result, Token, Type};
|
||||
|
||||
struct FallibleVariant {
|
||||
attributes: Vec<Attribute>,
|
||||
error_msg: LitStr,
|
||||
name: Ident,
|
||||
fields: Vec<(Ident, Type)>,
|
||||
}
|
||||
|
||||
impl FallibleVariant {
|
||||
fn has_no_source(&self) -> bool {
|
||||
self.attributes.iter().any(|attr| attr.path().is_ident("no_source"))
|
||||
}
|
||||
|
||||
fn should_generate_constructor(&self) -> bool {
|
||||
if self.has_no_source() {
|
||||
!self.fields.is_empty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FallibleInput {
|
||||
enum_name: Ident,
|
||||
variants: Vec<FallibleVariant>,
|
||||
}
|
||||
|
||||
impl Parse for FallibleInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let enum_name = input.parse::<Ident>()?;
|
||||
|
||||
let content;
|
||||
syn::braced!(content in input);
|
||||
|
||||
let mut variants = Vec::new();
|
||||
|
||||
while !content.is_empty() {
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
while content.peek(Token![#]) {
|
||||
attributes.push(content.call(Attribute::parse_outer)?);
|
||||
}
|
||||
|
||||
let attributes: Vec<_> = attributes.into_iter().flatten().collect();
|
||||
|
||||
let error_attr = attributes
|
||||
.iter()
|
||||
.find(|attr| attr.path().is_ident("error"))
|
||||
.ok_or_else(|| Error::new(content.span(), "Missing #[error] attribute"))?;
|
||||
|
||||
let error_msg = match &error_attr.meta {
|
||||
syn::Meta::List(list) => syn::parse2::<LitStr>(list.tokens.clone())?,
|
||||
_ => {
|
||||
return Err(Error::new(error_attr.span(), "Invalid error attribute format"));
|
||||
}
|
||||
};
|
||||
|
||||
let name = content.parse::<Ident>()?;
|
||||
|
||||
let mut fields = Vec::new();
|
||||
if content.peek(syn::token::Brace) {
|
||||
let fields_content;
|
||||
syn::braced!(fields_content in content);
|
||||
|
||||
while !fields_content.is_empty() {
|
||||
let field_name = fields_content.parse::<Ident>()?;
|
||||
fields_content.parse::<Token![:]>()?;
|
||||
let field_type = fields_content.parse::<Type>()?;
|
||||
fields.push((field_name, field_type));
|
||||
|
||||
if !fields_content.is_empty() {
|
||||
fields_content.parse::<Token![,]>()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !content.is_empty() {
|
||||
content.parse::<Token![,]>()?;
|
||||
}
|
||||
|
||||
variants.push(FallibleVariant {
|
||||
attributes,
|
||||
error_msg,
|
||||
name,
|
||||
fields,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(FallibleInput { enum_name, variants })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fallible_impl(input: TokenStream) -> TokenStream {
|
||||
let input = syn::parse_macro_input!(input as FallibleInput);
|
||||
|
||||
let enum_name = &input.enum_name;
|
||||
let variants = &input.variants;
|
||||
|
||||
let enum_variants = variants.iter().map(|variant| {
|
||||
let name = &variant.name;
|
||||
let error_msg = &variant.error_msg;
|
||||
let fields = &variant.fields;
|
||||
|
||||
let field_definitions = fields.iter().map(|(name, ty)| {
|
||||
quote! { #name: #ty }
|
||||
});
|
||||
|
||||
if variant.has_no_source() {
|
||||
if variant.fields.is_empty() {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name { #(#field_definitions,)* }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name {
|
||||
#(#field_definitions,)*
|
||||
err: String
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let constructors = variants.iter().filter_map(|variant| {
|
||||
if !variant.should_generate_constructor() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = &variant.name;
|
||||
let fields = &variant.fields;
|
||||
|
||||
let params = fields.iter().map(|(field_name, field_type)| {
|
||||
quote! { #field_name: impl Into<#field_type> }
|
||||
});
|
||||
|
||||
let field_assignments = fields.iter().map(|(field_name, _)| {
|
||||
quote! { #field_name: #field_name.into() }
|
||||
});
|
||||
|
||||
if variant.has_no_source() {
|
||||
Some(quote! {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #name(#(#params),*) -> Self {
|
||||
Self::#name {
|
||||
#(#field_assignments,)*
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(quote! {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #name(#(#params,)* source: impl std::fmt::Display) -> Self {
|
||||
Self::#name {
|
||||
#(#field_assignments,)*
|
||||
err: source.to_string()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let expanded = quote! {
|
||||
#[allow(dead_code, clippy::enum_variant_names)]
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum #enum_name {
|
||||
#(#enum_variants,)*
|
||||
}
|
||||
|
||||
impl #enum_name {
|
||||
#(#constructors)*
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
33
macros/src/lib.rs
Normal file
@ -0,0 +1,33 @@
|
||||
mod config;
|
||||
mod error_enum;
|
||||
mod fallible;
|
||||
mod log;
|
||||
mod loggable;
|
||||
mod traceable;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn config_settings(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
config::config_settings_impl(attr, item)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn fallible(input: TokenStream) -> TokenStream {
|
||||
fallible::fallible_impl(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn log(input: TokenStream) -> TokenStream {
|
||||
log::log_impl(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn loggable(input: TokenStream) -> TokenStream {
|
||||
loggable::loggable_impl(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn traceable(input: TokenStream) -> TokenStream {
|
||||
traceable::traceable_impl(input)
|
||||
}
|
||||
66
macros/src/log.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{Expr, Token, parse_macro_input};
|
||||
|
||||
struct LogInput {
|
||||
error: Expr,
|
||||
debug_info: Option<Expr>,
|
||||
}
|
||||
|
||||
impl Parse for LogInput {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let error = input.parse::<Expr>()?;
|
||||
|
||||
let debug_info = if input.peek(Token![,]) {
|
||||
input.parse::<Token![,]>()?;
|
||||
Some(input.parse::<Expr>()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(LogInput { error, debug_info })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_impl(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as LogInput);
|
||||
|
||||
let error_expr = &input.error;
|
||||
|
||||
if let Some(debug_info) = &input.debug_info {
|
||||
quote! {
|
||||
{
|
||||
let error = #error_expr;
|
||||
let level = error.level();
|
||||
let message = error.to_string();
|
||||
let debug_info = #debug_info;
|
||||
|
||||
match level {
|
||||
tracing::Level::ERROR => tracing::error!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::WARN => tracing::warn!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::INFO => tracing::info!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::DEBUG => tracing::debug!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::TRACE => tracing::trace!(message = %message, debug = ?debug_info),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
{
|
||||
let error = #error_expr;
|
||||
let level = error.level();
|
||||
let message = error.to_string();
|
||||
|
||||
match level {
|
||||
tracing::Level::ERROR => tracing::error!("{}", message),
|
||||
tracing::Level::WARN => tracing::warn!("{}", message),
|
||||
tracing::Level::INFO => tracing::info!("{}", message),
|
||||
tracing::Level::DEBUG => tracing::debug!("{}", message),
|
||||
tracing::Level::TRACE => tracing::trace!("{}", message),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
7
macros/src/loggable.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
use crate::error_enum;
|
||||
|
||||
pub fn loggable_impl(input: TokenStream) -> TokenStream {
|
||||
error_enum::generate_error_enum(input, true)
|
||||
}
|
||||
7
macros/src/traceable.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
use crate::error_enum;
|
||||
|
||||
pub fn traceable_impl(input: TokenStream) -> TokenStream {
|
||||
error_enum::generate_error_enum(input, false)
|
||||
}
|
||||
BIN
models/classifier.onnx
Normal file
BIN
models/deep_autoencoder.onnx
Normal file
336
models/full_config.json
Normal file
@ -0,0 +1,336 @@
|
||||
{
|
||||
"created_at": "2026-04-07T02:27:17.281069",
|
||||
"framework": "PyTorch",
|
||||
"model_type": "pipeline",
|
||||
"model": {
|
||||
"deep_autoencoder": {
|
||||
"file": "deep_autoencoder.onnx",
|
||||
"input_dim": 31,
|
||||
"encoding_dim": 12,
|
||||
"ae_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts",
|
||||
"fwd_iat_std",
|
||||
"bwd_iat_std",
|
||||
"fwd_bwd_bytes_ratio",
|
||||
"iat_cv"
|
||||
],
|
||||
"ae_threshold": 0.23011694848537445
|
||||
},
|
||||
"classifier": {
|
||||
"file": "classifier.onnx",
|
||||
"type": "classifier",
|
||||
"n_features": 32,
|
||||
"n_classes": 10,
|
||||
"outputs": [
|
||||
"anomaly",
|
||||
"class_probs",
|
||||
"c2_score"
|
||||
],
|
||||
"classifier_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts",
|
||||
"fwd_iat_std",
|
||||
"bwd_iat_std",
|
||||
"fwd_bwd_bytes_ratio",
|
||||
"iat_cv",
|
||||
"ae_anomaly_score"
|
||||
]
|
||||
}
|
||||
},
|
||||
"preprocessing": {
|
||||
"ae_clip_params": {
|
||||
"flow_duration": {
|
||||
"lower": 0.0,
|
||||
"upper": 115669365.2
|
||||
},
|
||||
"fwd_packets": {
|
||||
"lower": 0.0,
|
||||
"upper": 120.0
|
||||
},
|
||||
"bwd_packets": {
|
||||
"lower": 0.0,
|
||||
"upper": 126.0
|
||||
},
|
||||
"fwd_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 19557.400390625
|
||||
},
|
||||
"bwd_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 85164.0
|
||||
},
|
||||
"flow_bytes_per_sec": {
|
||||
"lower": 0.0,
|
||||
"upper": 1627586.8125000005
|
||||
},
|
||||
"flow_pkts_per_sec": {
|
||||
"lower": 0.0,
|
||||
"upper": 23809.5234375
|
||||
},
|
||||
"fwd_win_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 65280.0
|
||||
},
|
||||
"bwd_win_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 65535.0
|
||||
},
|
||||
"fwd_pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 1500.0
|
||||
},
|
||||
"bwd_pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 822.0007794189461
|
||||
},
|
||||
"fwd_iat_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 54051113.24
|
||||
},
|
||||
"bwd_iat_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 6912790.715000001
|
||||
},
|
||||
"flow_iat_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 166521472.0
|
||||
},
|
||||
"pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 957.2035284423835
|
||||
},
|
||||
"dst_port": {
|
||||
"lower": 0.0,
|
||||
"upper": 63005.0
|
||||
},
|
||||
"protocol": {
|
||||
"lower": 0.0,
|
||||
"upper": 17.0
|
||||
},
|
||||
"psh_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 52.0
|
||||
},
|
||||
"ack_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 107.0
|
||||
},
|
||||
"syn_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 4.0
|
||||
},
|
||||
"fin_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 1.0
|
||||
},
|
||||
"rst_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 0.0
|
||||
},
|
||||
"pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 818.4579974365238
|
||||
},
|
||||
"fwd_pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 256.8401712036142
|
||||
},
|
||||
"bwd_pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 676.0667114257812
|
||||
},
|
||||
"fwd_seg_size_min": {
|
||||
"lower": 0.0,
|
||||
"upper": 1026743.0693750025
|
||||
},
|
||||
"fwd_act_data_pkts": {
|
||||
"lower": 0.0,
|
||||
"upper": 12.0
|
||||
},
|
||||
"fwd_iat_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 6691987.085000001
|
||||
},
|
||||
"bwd_iat_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 5136363.065000001
|
||||
},
|
||||
"fwd_bwd_bytes_ratio": {
|
||||
"lower": 0.0,
|
||||
"upper": 1.0
|
||||
},
|
||||
"iat_cv": {
|
||||
"lower": 0.0,
|
||||
"upper": 0.0
|
||||
}
|
||||
},
|
||||
"ae_scaler": {
|
||||
"mean": [
|
||||
1775639.8280735926,
|
||||
5.542641564702501,
|
||||
4.873551306800804,
|
||||
673.3628917399571,
|
||||
1776.7162625946232,
|
||||
22595.98254433581,
|
||||
457.7205079100132,
|
||||
5370.204067202503,
|
||||
9979.409366405764,
|
||||
310.21111530262294,
|
||||
64.0225296706552,
|
||||
421007.8046985764,
|
||||
74954.24060464761,
|
||||
1927814.4699678936,
|
||||
18.27810422291583,
|
||||
10009.108501605231,
|
||||
9.275900254913996,
|
||||
0.6270530398000678,
|
||||
1.4141691028300247,
|
||||
0.08438195832759936,
|
||||
0.0413846397252831,
|
||||
0.0,
|
||||
20.458150398533718,
|
||||
3.321724142251631,
|
||||
13.033036407393814,
|
||||
8569.843223681366,
|
||||
0.21485243990919378,
|
||||
94146.99057411935,
|
||||
68775.38334652747,
|
||||
0.47595050130443944,
|
||||
0.0
|
||||
],
|
||||
"std": [
|
||||
12196313.175317517,
|
||||
13.15004546194127,
|
||||
13.245688945052057,
|
||||
1969.9160973703263,
|
||||
8250.018585629565,
|
||||
161540.2407042193,
|
||||
2518.5714986775442,
|
||||
12103.605209582642,
|
||||
20308.80085611352,
|
||||
476.8868336260097,
|
||||
95.72217224515282,
|
||||
4199427.656867863,
|
||||
616719.9756782106,
|
||||
16437665.614047276,
|
||||
97.4688620675193,
|
||||
18194.33523865822,
|
||||
5.182633726939228,
|
||||
4.994559084102198,
|
||||
10.431975160428792,
|
||||
0.558462828085695,
|
||||
0.19916731632770637,
|
||||
1.0,
|
||||
106.06236469581468,
|
||||
24.362268530572912,
|
||||
85.37603561474889,
|
||||
84340.18813365103,
|
||||
1.4442281045462682,
|
||||
646928.7442307192,
|
||||
525550.9737726098,
|
||||
0.3431291415218137,
|
||||
1.0
|
||||
],
|
||||
"feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts",
|
||||
"fwd_iat_std",
|
||||
"bwd_iat_std",
|
||||
"fwd_bwd_bytes_ratio",
|
||||
"iat_cv"
|
||||
]
|
||||
},
|
||||
"post_scaling_clip": {
|
||||
"min": -5.0,
|
||||
"max": 5.0
|
||||
}
|
||||
},
|
||||
"attack_labels": {
|
||||
"0": "Bot",
|
||||
"1": "Brute Force",
|
||||
"2": "C2 Communication",
|
||||
"3": "DNS Tunneling",
|
||||
"4": "DoS\/DDoS",
|
||||
"5": "Exploitation",
|
||||
"6": "Malware",
|
||||
"7": "Normal",
|
||||
"8": "Reconnaissance",
|
||||
"9": "Web Attack"
|
||||
}
|
||||
}
|
||||
266
models/inference_config.json
Normal file
@ -0,0 +1,266 @@
|
||||
{
|
||||
"ae_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts",
|
||||
"fwd_iat_std",
|
||||
"bwd_iat_std",
|
||||
"fwd_bwd_bytes_ratio",
|
||||
"iat_cv"
|
||||
],
|
||||
"ae_clip_params": {
|
||||
"flow_duration": {
|
||||
"lower": 0,
|
||||
"upper": 115669365.2
|
||||
},
|
||||
"fwd_packets": {
|
||||
"lower": 0,
|
||||
"upper": 120
|
||||
},
|
||||
"bwd_packets": {
|
||||
"lower": 0,
|
||||
"upper": 126
|
||||
},
|
||||
"fwd_bytes": {
|
||||
"lower": 0,
|
||||
"upper": 19557.400390625
|
||||
},
|
||||
"bwd_bytes": {
|
||||
"lower": 0,
|
||||
"upper": 85164
|
||||
},
|
||||
"flow_bytes_per_sec": {
|
||||
"lower": 0,
|
||||
"upper": 1627586.8125000005
|
||||
},
|
||||
"flow_pkts_per_sec": {
|
||||
"lower": 0,
|
||||
"upper": 23809.5234375
|
||||
},
|
||||
"fwd_win_bytes": {
|
||||
"lower": 0,
|
||||
"upper": 65280
|
||||
},
|
||||
"bwd_win_bytes": {
|
||||
"lower": 0,
|
||||
"upper": 65535
|
||||
},
|
||||
"fwd_pkt_len_mean": {
|
||||
"lower": 0,
|
||||
"upper": 1500
|
||||
},
|
||||
"bwd_pkt_len_mean": {
|
||||
"lower": 0,
|
||||
"upper": 822.0007794189461
|
||||
},
|
||||
"fwd_iat_mean": {
|
||||
"lower": 0,
|
||||
"upper": 54051113.24
|
||||
},
|
||||
"bwd_iat_mean": {
|
||||
"lower": 0,
|
||||
"upper": 6912790.715000001
|
||||
},
|
||||
"flow_iat_mean": {
|
||||
"lower": 0,
|
||||
"upper": 166521472
|
||||
},
|
||||
"pkt_len_mean": {
|
||||
"lower": 0,
|
||||
"upper": 957.2035284423835
|
||||
},
|
||||
"dst_port": {
|
||||
"lower": 0,
|
||||
"upper": 63005
|
||||
},
|
||||
"protocol": {
|
||||
"lower": 0,
|
||||
"upper": 17
|
||||
},
|
||||
"psh_flag_cnt": {
|
||||
"lower": 0,
|
||||
"upper": 52
|
||||
},
|
||||
"ack_flag_cnt": {
|
||||
"lower": 0,
|
||||
"upper": 107
|
||||
},
|
||||
"syn_flag_cnt": {
|
||||
"lower": 0,
|
||||
"upper": 4
|
||||
},
|
||||
"fin_flag_cnt": {
|
||||
"lower": 0,
|
||||
"upper": 1
|
||||
},
|
||||
"rst_flag_cnt": {
|
||||
"lower": 0,
|
||||
"upper": 0
|
||||
},
|
||||
"pkt_len_std": {
|
||||
"lower": 0,
|
||||
"upper": 818.4579974365238
|
||||
},
|
||||
"fwd_pkt_len_std": {
|
||||
"lower": 0,
|
||||
"upper": 256.8401712036142
|
||||
},
|
||||
"bwd_pkt_len_std": {
|
||||
"lower": 0,
|
||||
"upper": 676.0667114257812
|
||||
},
|
||||
"fwd_seg_size_min": {
|
||||
"lower": 0,
|
||||
"upper": 1026743.0693750025
|
||||
},
|
||||
"fwd_act_data_pkts": {
|
||||
"lower": 0,
|
||||
"upper": 12
|
||||
},
|
||||
"fwd_iat_std": {
|
||||
"lower": 0,
|
||||
"upper": 6691987.085000001
|
||||
},
|
||||
"bwd_iat_std": {
|
||||
"lower": 0,
|
||||
"upper": 5136363.065000001
|
||||
},
|
||||
"fwd_bwd_bytes_ratio": {
|
||||
"lower": 0,
|
||||
"upper": 1
|
||||
},
|
||||
"iat_cv": {
|
||||
"lower": 0,
|
||||
"upper": 0
|
||||
}
|
||||
},
|
||||
"ae_scaler_mean": [
|
||||
1775639.8280735926,
|
||||
5.542641564702501,
|
||||
4.873551306800804,
|
||||
673.3628917399571,
|
||||
1776.7162625946232,
|
||||
22595.98254433581,
|
||||
457.7205079100132,
|
||||
5370.204067202503,
|
||||
9979.409366405764,
|
||||
310.21111530262294,
|
||||
64.0225296706552,
|
||||
421007.8046985764,
|
||||
74954.24060464761,
|
||||
1927814.4699678936,
|
||||
18.27810422291583,
|
||||
10009.108501605231,
|
||||
9.275900254913996,
|
||||
0.6270530398000678,
|
||||
1.4141691028300247,
|
||||
0.08438195832759936,
|
||||
0.0413846397252831,
|
||||
0,
|
||||
20.458150398533718,
|
||||
3.321724142251631,
|
||||
13.033036407393814,
|
||||
8569.843223681366,
|
||||
0.21485243990919378,
|
||||
94146.99057411935,
|
||||
68775.38334652747,
|
||||
0.47595050130443944,
|
||||
0
|
||||
],
|
||||
"ae_scaler_std": [
|
||||
12196313.175317517,
|
||||
13.15004546194127,
|
||||
13.245688945052057,
|
||||
1969.9160973703263,
|
||||
8250.018585629565,
|
||||
161540.2407042193,
|
||||
2518.5714986775442,
|
||||
12103.605209582642,
|
||||
20308.80085611352,
|
||||
476.8868336260097,
|
||||
95.72217224515282,
|
||||
4199427.656867863,
|
||||
616719.9756782106,
|
||||
16437665.614047276,
|
||||
97.4688620675193,
|
||||
18194.33523865822,
|
||||
5.182633726939228,
|
||||
4.994559084102198,
|
||||
10.431975160428792,
|
||||
0.558462828085695,
|
||||
0.19916731632770637,
|
||||
1,
|
||||
106.06236469581468,
|
||||
24.362268530572912,
|
||||
85.37603561474889,
|
||||
84340.18813365103,
|
||||
1.4442281045462682,
|
||||
646928.7442307192,
|
||||
525550.9737726098,
|
||||
0.3431291415218137,
|
||||
1
|
||||
],
|
||||
"ae_post_clip_min": -5,
|
||||
"ae_post_clip_max": 5,
|
||||
"classifier_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts",
|
||||
"fwd_iat_std",
|
||||
"bwd_iat_std",
|
||||
"fwd_bwd_bytes_ratio",
|
||||
"iat_cv",
|
||||
"ae_anomaly_score"
|
||||
],
|
||||
"minmax_params": {},
|
||||
"robust_params": {},
|
||||
"quantile_params": {}
|
||||
}
|
||||
182
models/manifest.yaml
Normal file
@ -0,0 +1,182 @@
|
||||
name: netguardia-v1
|
||||
version: 1
|
||||
|
||||
runtime:
|
||||
pipeline_mode: dag
|
||||
normal_label: Normal
|
||||
|
||||
artifacts:
|
||||
- id: anomaly_detector_onnx
|
||||
file: deep_autoencoder.onnx
|
||||
kind: onnx
|
||||
- id: classifier_onnx
|
||||
file: classifier.onnx
|
||||
kind: onnx
|
||||
- id: preprocessing_sidecar
|
||||
file: inference_config.json
|
||||
kind: sidecar
|
||||
|
||||
stages:
|
||||
- id: anomaly_detector
|
||||
kind: autoencoder
|
||||
model_file: deep_autoencoder.onnx
|
||||
inputs:
|
||||
- { name: flow_duration, source: feature }
|
||||
- { name: fwd_packets, source: feature }
|
||||
- { name: bwd_packets, source: feature }
|
||||
- { name: fwd_bytes, source: feature }
|
||||
- { name: bwd_bytes, source: feature }
|
||||
- { name: flow_bytes_per_sec, source: feature }
|
||||
- { name: flow_pkts_per_sec, source: feature }
|
||||
- { name: fwd_win_bytes, source: feature }
|
||||
- { name: bwd_win_bytes, source: feature }
|
||||
- { name: fwd_pkt_len_mean, source: feature }
|
||||
- { name: bwd_pkt_len_mean, source: feature }
|
||||
- { name: fwd_iat_mean, source: feature }
|
||||
- { name: bwd_iat_mean, source: feature }
|
||||
- { name: flow_iat_mean, source: feature }
|
||||
- { name: pkt_len_mean, source: feature }
|
||||
- { name: dst_port, source: feature }
|
||||
- { name: protocol, source: feature }
|
||||
- { name: psh_flag_cnt, source: feature }
|
||||
- { name: ack_flag_cnt, source: feature }
|
||||
- { name: syn_flag_cnt, source: feature }
|
||||
- { name: fin_flag_cnt, source: feature }
|
||||
- { name: rst_flag_cnt, source: feature }
|
||||
- { name: pkt_len_std, source: feature }
|
||||
- { name: fwd_pkt_len_std, source: feature }
|
||||
- { name: bwd_pkt_len_std, source: feature }
|
||||
- { name: fwd_seg_size_min, source: feature }
|
||||
- { name: fwd_act_data_pkts, source: feature }
|
||||
- { name: fwd_iat_std, source: feature }
|
||||
- { name: bwd_iat_std, source: feature }
|
||||
- { name: fwd_bwd_bytes_ratio, source: feature }
|
||||
- { name: iat_cv, source: feature }
|
||||
preprocessing:
|
||||
- type: standard_scaler
|
||||
sidecar: inference_config.json
|
||||
- type: clip
|
||||
min: -5.0
|
||||
max: 5.0
|
||||
output_heads:
|
||||
- name: ae_anomaly_score
|
||||
index: 0
|
||||
shape: [1]
|
||||
semantic: anomaly_score
|
||||
threshold: 0.23011694848537445
|
||||
|
||||
- id: classifier
|
||||
kind: classifier
|
||||
model_file: classifier.onnx
|
||||
depends_on:
|
||||
- anomaly_detector
|
||||
inputs:
|
||||
- { name: flow_duration, source: feature }
|
||||
- { name: fwd_packets, source: feature }
|
||||
- { name: bwd_packets, source: feature }
|
||||
- { name: fwd_bytes, source: feature }
|
||||
- { name: bwd_bytes, source: feature }
|
||||
- { name: flow_bytes_per_sec, source: feature }
|
||||
- { name: flow_pkts_per_sec, source: feature }
|
||||
- { name: fwd_win_bytes, source: feature }
|
||||
- { name: bwd_win_bytes, source: feature }
|
||||
- { name: fwd_pkt_len_mean, source: feature }
|
||||
- { name: bwd_pkt_len_mean, source: feature }
|
||||
- { name: fwd_iat_mean, source: feature }
|
||||
- { name: bwd_iat_mean, source: feature }
|
||||
- { name: flow_iat_mean, source: feature }
|
||||
- { name: pkt_len_mean, source: feature }
|
||||
- { name: dst_port, source: feature }
|
||||
- { name: protocol, source: feature }
|
||||
- { name: psh_flag_cnt, source: feature }
|
||||
- { name: ack_flag_cnt, source: feature }
|
||||
- { name: syn_flag_cnt, source: feature }
|
||||
- { name: fin_flag_cnt, source: feature }
|
||||
- { name: rst_flag_cnt, source: feature }
|
||||
- { name: pkt_len_std, source: feature }
|
||||
- { name: fwd_pkt_len_std, source: feature }
|
||||
- { name: bwd_pkt_len_std, source: feature }
|
||||
- { name: fwd_seg_size_min, source: feature }
|
||||
- { name: fwd_act_data_pkts, source: feature }
|
||||
- { name: fwd_iat_std, source: feature }
|
||||
- { name: bwd_iat_std, source: feature }
|
||||
- { name: fwd_bwd_bytes_ratio, source: feature }
|
||||
- { name: iat_cv, source: feature }
|
||||
- name: ae_anomaly_score
|
||||
source: stage_output
|
||||
stage: anomaly_detector
|
||||
output: ae_anomaly_score
|
||||
output_heads:
|
||||
- name: anomaly
|
||||
index: 0
|
||||
shape: [1]
|
||||
semantic: binary
|
||||
threshold: 0.9179317355155945
|
||||
- name: class_probs
|
||||
index: 1
|
||||
shape: [10]
|
||||
semantic: multiclass
|
||||
min_confidence: 0.4
|
||||
- name: c2_score
|
||||
index: 2
|
||||
shape: [1]
|
||||
semantic: binary
|
||||
threshold: 0.9085615873336792
|
||||
|
||||
outputs:
|
||||
- stage: anomaly_detector
|
||||
output: ae_anomaly_score
|
||||
alias: ae_anomaly_score
|
||||
role: anomaly_score
|
||||
- stage: classifier
|
||||
output: anomaly
|
||||
alias: anomaly
|
||||
role: binary_score
|
||||
- stage: classifier
|
||||
output: class_probs
|
||||
alias: class_probs
|
||||
role: class_probabilities
|
||||
- stage: classifier
|
||||
output: c2_score
|
||||
alias: c2_score
|
||||
role: c2_score
|
||||
|
||||
detection_rules:
|
||||
- id: classifier_anomaly_threshold
|
||||
type: threshold
|
||||
output: anomaly
|
||||
attack:
|
||||
source: predicted_class
|
||||
output: class_probs
|
||||
exclude_normal: true
|
||||
- id: class_confidence
|
||||
type: class_confidence
|
||||
output: class_probs
|
||||
attack:
|
||||
source: predicted_class
|
||||
output: class_probs
|
||||
exclude_normal: true
|
||||
- id: c2_threshold
|
||||
type: threshold
|
||||
output: c2_score
|
||||
attack:
|
||||
source: fixed_label
|
||||
label: C2 Communication
|
||||
|
||||
labels:
|
||||
"0": { name: Bot, confirmations: 1 }
|
||||
"1": { name: Brute Force }
|
||||
"2": { name: C2 Communication, confirmations: 1 }
|
||||
"3": { name: DNS Tunneling, confirmations: 1 }
|
||||
"4": { name: DoS/DDoS, confirmations: 2 }
|
||||
"5": { name: Exploitation, confirmations: 1 }
|
||||
"6": { name: Malware }
|
||||
"7": { name: Normal }
|
||||
"8": { name: Reconnaissance }
|
||||
"9": { name: Web Attack }
|
||||
|
||||
alert_rules:
|
||||
- condition: "anomaly > threshold"
|
||||
source_label: anomaly
|
||||
- condition: "class_probs.argmax != Normal AND class_probs.max > min_confidence"
|
||||
source_label: class_probs
|
||||
@ -1,15 +1,17 @@
|
||||
[package]
|
||||
name = "net-guardia-common"
|
||||
name = "net-guardia-abi"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
user = ["aya"]
|
||||
user = ["aya", "serde"]
|
||||
kernel = []
|
||||
|
||||
[dependencies]
|
||||
aya = { workspace = true, optional = true }
|
||||
network-types = "0.0.7"
|
||||
serde = { workspace = true, optional = true }
|
||||
network-types = { workspace = true }
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
8
net-guardia-abi/src/define/drop_reason.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub const DROP_REASON_ACL_BLACKLIST: u8 = 1;
|
||||
pub const DROP_REASON_RATE_LIMIT_PKT: u8 = 2;
|
||||
pub const DROP_REASON_RATE_LIMIT_SYN: u8 = 3;
|
||||
pub const DROP_REASON_RATE_LIMIT_UDP: u8 = 4;
|
||||
pub const DROP_REASON_RATE_LIMIT_DNS: u8 = 5;
|
||||
pub const DROP_REASON_PROTOCOL_FILTER: u8 = 6;
|
||||
pub const DROP_REASON_GEO_BLOCK: u8 = 7;
|
||||
pub const DROP_REASON_DNS_BLACKLIST: u8 = 8;
|
||||
6
net-guardia-abi/src/define/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod drop_reason;
|
||||
pub mod offset;
|
||||
pub mod pipeline;
|
||||
pub mod rate_limit;
|
||||
pub mod setting;
|
||||
pub mod tcp_flags;
|
||||
34
net-guardia-abi/src/define/offset.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use core::mem::size_of;
|
||||
|
||||
use network_types::eth::EthHdr;
|
||||
use network_types::ip::{Ipv4Hdr, Ipv6Hdr};
|
||||
use network_types::tcp::TcpHdr;
|
||||
use network_types::udp::UdpHdr;
|
||||
|
||||
pub const ETHER_HEADER_START: usize = 0;
|
||||
pub const ETHER_HEADER_END: usize = ETHER_HEADER_START + size_of::<EthHdr>();
|
||||
|
||||
pub const IPV4_HEADER_START: usize = ETHER_HEADER_END;
|
||||
pub const IPV4_HEADER_END: usize = IPV4_HEADER_START + size_of::<Ipv4Hdr>();
|
||||
|
||||
pub const IPV6_HEADER_START: usize = ETHER_HEADER_END;
|
||||
pub const IPV6_HEADER_END: usize = IPV6_HEADER_START + size_of::<Ipv6Hdr>();
|
||||
|
||||
pub const IPV4_TCP_HEADER_START: usize = IPV4_HEADER_END;
|
||||
pub const IPV4_TCP_HEADER_END: usize = IPV4_TCP_HEADER_START + size_of::<TcpHdr>();
|
||||
|
||||
pub const IPV6_TCP_HEADER_START: usize = IPV6_HEADER_END;
|
||||
pub const IPV6_TCP_HEADER_END: usize = IPV6_TCP_HEADER_START + size_of::<TcpHdr>();
|
||||
|
||||
pub const IPV4_UDP_HEADER_START: usize = IPV4_HEADER_END;
|
||||
pub const IPV4_UDP_HEADER_END: usize = IPV4_UDP_HEADER_START + size_of::<UdpHdr>();
|
||||
|
||||
pub const IPV6_UDP_HEADER_START: usize = IPV6_HEADER_END;
|
||||
pub const IPV6_UDP_HEADER_END: usize = IPV6_UDP_HEADER_START + size_of::<UdpHdr>();
|
||||
|
||||
#[cfg(not(feature = "user"))]
|
||||
const _: () = {
|
||||
assert!(size_of::<EthHdr>() == 14);
|
||||
assert!(size_of::<Ipv4Hdr>() == 20);
|
||||
assert!(size_of::<Ipv6Hdr>() == 40);
|
||||
};
|
||||
7
net-guardia-abi/src/define/pipeline.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub const MAX_STAGES: u32 = 8;
|
||||
pub const STAGE_NONE: u32 = u32::MAX;
|
||||
pub const STAGE_ENTRY: u32 = 0;
|
||||
pub const STAGE_ACCESS_CONTROL: u32 = 1;
|
||||
pub const STAGE_RATE_LIMIT: u32 = 2;
|
||||
pub const STAGE_SERVICE: u32 = 3;
|
||||
pub const STAGE_TRANSMISSION: u32 = 7;
|
||||
5
net-guardia-abi/src/define/rate_limit.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub const CFG_PACKET_RATE: u32 = 0;
|
||||
pub const CFG_SYN_RATE: u32 = 1;
|
||||
pub const CFG_UDP_RATE: u32 = 2;
|
||||
pub const CFG_DNS_RATE: u32 = 3;
|
||||
pub const CFG_WINDOW_NS: u32 = 4;
|
||||
10
net-guardia-abi/src/define/setting.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub const MAX_STATS: usize = 131072;
|
||||
pub const MAX_RULES: usize = 128;
|
||||
pub const MAX_RULES_PORT: usize = 32;
|
||||
pub const MAX_TRACKED_IPS: u32 = 65536;
|
||||
pub const DEFAULT_WINDOW_NS: u64 = 1_000_000_000;
|
||||
pub const DEFAULT_PACKET_RATE: u64 = 10000;
|
||||
pub const DEFAULT_SYN_RATE: u64 = 100;
|
||||
pub const DEFAULT_UDP_RATE: u64 = 5000;
|
||||
pub const DEFAULT_DNS_RATE: u64 = 200;
|
||||
pub const MAX_GEO_ENTRIES: u32 = 131072;
|
||||
8
net-guardia-abi/src/define/tcp_flags.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub const TCP_FIN: u8 = 0x01;
|
||||
pub const TCP_SYN: u8 = 0x02;
|
||||
pub const TCP_RST: u8 = 0x04;
|
||||
pub const TCP_PSH: u8 = 0x08;
|
||||
pub const TCP_ACK: u8 = 0x10;
|
||||
pub const TCP_URG: u8 = 0x20;
|
||||
pub const TCP_ECE: u8 = 0x40;
|
||||
pub const TCP_CWR: u8 = 0x80;
|
||||
2
net-guardia-abi/src/ebpf/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod parsing;
|
||||
pub mod symmetric_hash;
|
||||
186
net-guardia-abi/src/ebpf/parsing.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use core::mem::size_of;
|
||||
use core::ptr;
|
||||
|
||||
use network_types::eth::{EthHdr, EtherType};
|
||||
use network_types::ip::{IpProto, Ipv4Hdr, Ipv6Hdr};
|
||||
use network_types::tcp::TcpHdr;
|
||||
use network_types::udp::UdpHdr;
|
||||
|
||||
use crate::define::offset::*;
|
||||
use crate::model::ip_address::IpVersion;
|
||||
use crate::model::parsed_packet::ParsedPacket;
|
||||
|
||||
pub unsafe fn parse_packet(start: usize, end: usize, target: *mut ParsedPacket) -> Option<()> {
|
||||
unsafe {
|
||||
if start + ETHER_HEADER_END > end {
|
||||
return None;
|
||||
}
|
||||
let eth = &*((start + ETHER_HEADER_START) as *const EthHdr);
|
||||
let ether_type = eth.ether_type().ok()?;
|
||||
match ether_type {
|
||||
EtherType::Ipv4 => parse_ipv4_packet(start, end, target),
|
||||
EtherType::Ipv6 => parse_ipv6_packet(start, end, target),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_ipv4_packet(start: usize, end: usize, target: *mut ParsedPacket) -> Option<()> {
|
||||
if start + IPV4_HEADER_END > end {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ipv4 = &*((start + IPV4_HEADER_START) as *const Ipv4Hdr);
|
||||
let ipv4_header_len = parse_ipv4_header_len(start, end)?;
|
||||
let l4_start = IPV4_HEADER_START + ipv4_header_len;
|
||||
let ip_total_len = read_be_u16(start, end, IPV4_HEADER_START + 2)? as usize;
|
||||
if ip_total_len < ipv4_header_len {
|
||||
return None;
|
||||
}
|
||||
let transport_len = ip_total_len - ipv4_header_len;
|
||||
let packet_length = ip_total_len as u32;
|
||||
|
||||
let t = &mut *target;
|
||||
ptr::copy_nonoverlapping(ipv4.src_addr.as_ptr(), t.src_ip.as_mut_ptr(), 4);
|
||||
ptr::copy_nonoverlapping(ipv4.dst_addr.as_ptr(), t.dst_ip.as_mut_ptr(), 4);
|
||||
t.packet_length = packet_length;
|
||||
t.ip_version = IpVersion::V4.as_u8();
|
||||
t.protocol = ipv4.proto;
|
||||
|
||||
let (src_port, dst_port, tcp_flags, l4_header_len, transport_len) = match ipv4.proto {
|
||||
value if value == IpProto::Tcp as u8 => parse_tcp(start, end, l4_start, transport_len)?,
|
||||
value if value == IpProto::Udp as u8 => parse_udp(start, end, l4_start, transport_len)?,
|
||||
_ => (0, 0, 0, 0, 0),
|
||||
};
|
||||
|
||||
t.payload_length = (transport_len as u32).saturating_sub(l4_header_len as u32);
|
||||
t.src_port = src_port;
|
||||
t.dst_port = dst_port;
|
||||
t.tcp_flags = tcp_flags;
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_ipv6_packet(start: usize, end: usize, target: *mut ParsedPacket) -> Option<()> {
|
||||
if start + IPV6_HEADER_END > end {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ipv6 = &*((start + IPV6_HEADER_START) as *const Ipv6Hdr);
|
||||
let payload_len = read_be_u16(start, end, IPV6_HEADER_START + 4)? as usize;
|
||||
let packet_length = (IPV6_HEADER_END - IPV6_HEADER_START + payload_len) as u32;
|
||||
|
||||
let t = &mut *target;
|
||||
ptr::copy_nonoverlapping(ipv6.src_addr.as_ptr(), t.src_ip.as_mut_ptr(), 16);
|
||||
ptr::copy_nonoverlapping(ipv6.dst_addr.as_ptr(), t.dst_ip.as_mut_ptr(), 16);
|
||||
t.packet_length = packet_length;
|
||||
t.ip_version = IpVersion::V6.as_u8();
|
||||
t.protocol = ipv6.next_hdr;
|
||||
|
||||
let (src_port, dst_port, tcp_flags, l4_header_len, transport_len) = match ipv6.next_hdr {
|
||||
value if value == IpProto::Tcp as u8 => parse_tcp(start, end, IPV6_TCP_HEADER_START, payload_len)?,
|
||||
value if value == IpProto::Udp as u8 => parse_udp(start, end, IPV6_UDP_HEADER_START, payload_len)?,
|
||||
_ => (0, 0, 0, 0, 0),
|
||||
};
|
||||
|
||||
t.payload_length = (transport_len as u32).saturating_sub(l4_header_len as u32);
|
||||
t.src_port = src_port;
|
||||
t.dst_port = dst_port;
|
||||
t.tcp_flags = tcp_flags;
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_ipv4_header_len(start: usize, end: usize) -> Option<usize> {
|
||||
if start + IPV4_HEADER_START + 1 > end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version_ihl = unsafe { *((start + IPV4_HEADER_START) as *const u8) };
|
||||
let version = version_ihl >> 4;
|
||||
let ihl = (version_ihl & 0x0f) as usize;
|
||||
if version != 4 || !(5..=15).contains(&ihl) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let header_len = ihl * 4;
|
||||
if start + IPV4_HEADER_START + header_len > end {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(header_len)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_tcp(
|
||||
start: usize,
|
||||
end: usize,
|
||||
tcp_start: usize,
|
||||
transport_len: usize,
|
||||
) -> Option<(u16, u16, u8, usize, usize)> {
|
||||
if start + tcp_start + size_of::<TcpHdr>() > end {
|
||||
return None;
|
||||
}
|
||||
if transport_len < size_of::<TcpHdr>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let tcp = &*((start + tcp_start) as *const TcpHdr);
|
||||
let data_offset = (*((start + tcp_start + 12) as *const u8) >> 4) as usize;
|
||||
if !(5..=15).contains(&data_offset) {
|
||||
return None;
|
||||
}
|
||||
let header_len = data_offset * 4;
|
||||
if header_len > transport_len || start + tcp_start + header_len > end {
|
||||
return None;
|
||||
}
|
||||
let flags = *((start + tcp_start + 13) as *const u8);
|
||||
Some((
|
||||
u16::from_be_bytes(tcp.source),
|
||||
u16::from_be_bytes(tcp.dest),
|
||||
flags,
|
||||
header_len,
|
||||
transport_len,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_udp(
|
||||
start: usize,
|
||||
end: usize,
|
||||
udp_start: usize,
|
||||
transport_len: usize,
|
||||
) -> Option<(u16, u16, u8, usize, usize)> {
|
||||
if start + udp_start + size_of::<UdpHdr>() > end {
|
||||
return None;
|
||||
}
|
||||
if transport_len < size_of::<UdpHdr>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let udp = unsafe { &*((start + udp_start) as *const UdpHdr) };
|
||||
let udp_len = udp.len() as usize;
|
||||
if udp_len < size_of::<UdpHdr>() || udp_len > transport_len {
|
||||
return None;
|
||||
}
|
||||
Some((udp.src_port(), udp.dst_port(), 0u8, 8usize, udp_len))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn read_be_u16(start: usize, end: usize, offset: usize) -> Option<u16> {
|
||||
if start + offset + 2 > end {
|
||||
return None;
|
||||
}
|
||||
let hi = unsafe { *((start + offset) as *const u8) };
|
||||
let lo = unsafe { *((start + offset + 1) as *const u8) };
|
||||
Some(u16::from_be_bytes([hi, lo]))
|
||||
}
|
||||
25
net-guardia-abi/src/ebpf/symmetric_hash.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crate::model::ip_address::IpVersion;
|
||||
use crate::model::parsed_packet::ParsedPacket;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn symmetric_queue_id(pkt: &ParsedPacket, num_queues: u32) -> Option<u32> {
|
||||
if num_queues == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ip_hash = match pkt.ip_version {
|
||||
value if value == IpVersion::V4.as_u8() => pkt.src_ip_v4() ^ pkt.dst_ip_v4(),
|
||||
value if value == IpVersion::V6.as_u8() => {
|
||||
let s = pkt.src_ip_v6();
|
||||
let d = pkt.dst_ip_v6();
|
||||
let xor = s ^ d;
|
||||
(xor as u32) ^ ((xor >> 32) as u32) ^ ((xor >> 64) as u32) ^ ((xor >> 96) as u32)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let port_hash = (pkt.src_port as u32) ^ (pkt.dst_port as u32);
|
||||
|
||||
let h = (ip_hash ^ port_hash.rotate_left(16) ^ pkt.protocol as u32).wrapping_mul(2654435761);
|
||||
|
||||
Some(h % num_queues)
|
||||
}
|
||||
9
net-guardia-abi/src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
||||
#![no_std]
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
extern crate std;
|
||||
|
||||
pub mod define;
|
||||
#[cfg(feature = "kernel")]
|
||||
pub mod ebpf;
|
||||
pub mod model;
|
||||
17
net-guardia-abi/src/model/dns_name.rs
Normal file
@ -0,0 +1,17 @@
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DnsName {
|
||||
pub data: [u8; 128],
|
||||
}
|
||||
|
||||
impl DnsName {
|
||||
pub const fn zeroed() -> Self {
|
||||
Self { data: [0u8; 128] }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for DnsName {}
|
||||
13
net-guardia-abi/src/model/drop_event.rs
Normal file
@ -0,0 +1,13 @@
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DropEvent {
|
||||
pub timestamp_ns: u64,
|
||||
pub src_ip: [u8; 16],
|
||||
pub dst_ip: [u8; 16],
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub protocol: u8,
|
||||
pub reason: u8,
|
||||
pub ip_version: u8,
|
||||
pub _pad: u8,
|
||||
}
|
||||
1
net-guardia-abi/src/model/empty.rs
Normal file
@ -0,0 +1 @@
|
||||
pub type EmptyMapValue = u8;
|
||||
26
net-guardia-abi/src/model/flow_stats.rs
Normal file
@ -0,0 +1,26 @@
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
#[cfg(feature = "user")]
|
||||
use serde::Serialize;
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Clone, Copy)]
|
||||
#[cfg_attr(feature = "user", derive(Serialize, Debug))]
|
||||
pub struct FlowStats {
|
||||
pub bytes: u64,
|
||||
pub packets: u64,
|
||||
pub last_seen: u64,
|
||||
}
|
||||
|
||||
impl FlowStats {
|
||||
pub fn new(bytes: u64, packets: u64, last_seen: u64) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
packets,
|
||||
last_seen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for FlowStats {}
|
||||
@ -1,7 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use net_guardia_common::model::http_method::EbpfHttpMethod;
|
||||
#[cfg(feature = "user")]
|
||||
use std::vec::Vec;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
|
||||
#[cfg(feature = "user")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type HttpMethodBitmap = u16;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[cfg_attr(feature = "user", derive(Serialize, Deserialize, Debug, Eq, PartialEq))]
|
||||
pub enum HttpMethod {
|
||||
GET = 0b0000_0000_0000_0001,
|
||||
POST = 0b0000_0000_0000_0010,
|
||||
@ -14,9 +20,10 @@ pub enum HttpMethod {
|
||||
CONNECT = 0b0000_0001_0000_0000,
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
impl HttpMethod {
|
||||
pub fn convert_from_ebpf(ebpf_http_methods: EbpfHttpMethod) -> Vec<HttpMethod> {
|
||||
let value = ebpf_http_methods as u16;
|
||||
pub fn convert_from_bitmap(http_method_bitmap: HttpMethodBitmap) -> Vec<HttpMethod> {
|
||||
let value = http_method_bitmap;
|
||||
let mut http_methods = Vec::new();
|
||||
|
||||
let all_methods = [
|
||||
@ -39,7 +46,7 @@ impl HttpMethod {
|
||||
http_methods
|
||||
}
|
||||
|
||||
pub fn convert_to_ebpf(http_methods: Vec<HttpMethod>) -> EbpfHttpMethod {
|
||||
pub fn convert_to_bitmap(http_methods: Vec<HttpMethod>) -> HttpMethodBitmap {
|
||||
let mut ebpf_http_method = 0_u16;
|
||||
for http_method in http_methods {
|
||||
ebpf_http_method |= http_method as u16;
|
||||
139
net-guardia-abi/src/model/ip_address.rs
Normal file
@ -0,0 +1,139 @@
|
||||
use core::convert::TryFrom;
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
|
||||
pub type IPv4 = u32;
|
||||
pub type IPv6 = u128;
|
||||
pub type Port = u16;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum IpVersion {
|
||||
V4 = 4,
|
||||
V6 = 6,
|
||||
}
|
||||
|
||||
impl IpVersion {
|
||||
#[inline(always)]
|
||||
pub const fn as_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_u8(value: u8) -> Option<Self> {
|
||||
match value {
|
||||
4 => Some(Self::V4),
|
||||
6 => Some(Self::V6),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn is_v4(self) -> bool {
|
||||
matches!(self, Self::V4)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn is_v6(self) -> bool {
|
||||
matches!(self, Self::V6)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpVersion> for u8 {
|
||||
#[inline(always)]
|
||||
fn from(value: IpVersion) -> Self {
|
||||
value.as_u8()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for IpVersion {
|
||||
type Error = ();
|
||||
|
||||
#[inline(always)]
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
Self::from_u8(value).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
impl serde::Serialize for IpVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_u8(self.as_u8())
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AddrPortV4([u8; 8]);
|
||||
|
||||
impl AddrPortV4 {
|
||||
#[inline(always)]
|
||||
pub fn new(ip: u32, port: u16) -> Self {
|
||||
let mut key = [0u8; 8];
|
||||
key[0..4].copy_from_slice(&ip.to_ne_bytes());
|
||||
key[4..6].copy_from_slice(&port.to_ne_bytes());
|
||||
Self(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_bytes(&self) -> &[u8; 8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ip(&self) -> IPv4 {
|
||||
let mut ip_bytes = [0u8; 4];
|
||||
ip_bytes.copy_from_slice(&self.0[0..4]);
|
||||
u32::from_ne_bytes(ip_bytes)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn port(&self) -> Port {
|
||||
let mut port_bytes = [0u8; 2];
|
||||
port_bytes.copy_from_slice(&self.0[4..6]);
|
||||
u16::from_ne_bytes(port_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for AddrPortV4 {}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AddrPortV6([u8; 32]);
|
||||
|
||||
impl AddrPortV6 {
|
||||
#[inline(always)]
|
||||
pub fn new(ip: u128, port: u16) -> Self {
|
||||
let mut key = [0u8; 32];
|
||||
key[0..16].copy_from_slice(&ip.to_ne_bytes());
|
||||
key[16..18].copy_from_slice(&port.to_ne_bytes());
|
||||
Self(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ip(&self) -> IPv6 {
|
||||
let mut ip_bytes = [0u8; 16];
|
||||
ip_bytes.copy_from_slice(&self.0[0..16]);
|
||||
u128::from_ne_bytes(ip_bytes)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn port(&self) -> Port {
|
||||
let mut port_bytes = [0u8; 2];
|
||||
port_bytes.copy_from_slice(&self.0[16..18]);
|
||||
u16::from_ne_bytes(port_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for AddrPortV6 {}
|
||||
10
net-guardia-abi/src/model/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub mod dns_name;
|
||||
pub mod drop_event;
|
||||
pub mod empty;
|
||||
pub mod flow_stats;
|
||||
pub mod http_method;
|
||||
pub mod ip_address;
|
||||
pub mod parsed_packet;
|
||||
pub mod port_rule;
|
||||
pub mod pseudo_header;
|
||||
pub mod rate_limit;
|
||||
58
net-guardia-abi/src/model/parsed_packet.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use crate::model::ip_address::{AddrPortV4, AddrPortV6};
|
||||
|
||||
#[repr(C, align(8))]
|
||||
pub struct ParsedPacket {
|
||||
pub timestamp_ns: u64,
|
||||
pub src_ip: [u8; 16],
|
||||
pub dst_ip: [u8; 16],
|
||||
pub packet_length: u32,
|
||||
pub payload_length: u32,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub ip_version: u8,
|
||||
pub protocol: u8,
|
||||
pub tcp_flags: u8,
|
||||
pub _pad: u8,
|
||||
}
|
||||
|
||||
impl ParsedPacket {
|
||||
#[inline(always)]
|
||||
pub fn src_ip_v4(&self) -> u32 {
|
||||
u32::from_ne_bytes([self.src_ip[0], self.src_ip[1], self.src_ip[2], self.src_ip[3]])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn dst_ip_v4(&self) -> u32 {
|
||||
u32::from_ne_bytes([self.dst_ip[0], self.dst_ip[1], self.dst_ip[2], self.dst_ip[3]])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn src_ip_v6(&self) -> u128 {
|
||||
u128::from_ne_bytes(self.src_ip)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn dst_ip_v6(&self) -> u128 {
|
||||
u128::from_ne_bytes(self.dst_ip)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn src_addr_v4(&self) -> AddrPortV4 {
|
||||
AddrPortV4::new(self.src_ip_v4(), self.src_port)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn dst_addr_v4(&self) -> AddrPortV4 {
|
||||
AddrPortV4::new(self.dst_ip_v4(), self.dst_port)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn src_addr_v6(&self) -> AddrPortV6 {
|
||||
AddrPortV6::new(self.src_ip_v6(), self.src_port)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn dst_addr_v6(&self) -> AddrPortV6 {
|
||||
AddrPortV6::new(self.dst_ip_v6(), self.dst_port)
|
||||
}
|
||||
}
|
||||
111
net-guardia-abi/src/model/port_rule.rs
Normal file
@ -0,0 +1,111 @@
|
||||
#[cfg(feature = "user")]
|
||||
use std::vec::Vec;
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
|
||||
use crate::define::setting::MAX_RULES_PORT;
|
||||
use crate::model::ip_address::Port;
|
||||
|
||||
pub const PORT_RULE_MATCH_ALL: u8 = 1;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PortRule {
|
||||
pub match_all: u8,
|
||||
pub count: u8,
|
||||
pub _pad: [u8; 2],
|
||||
pub ports: [Port; MAX_RULES_PORT],
|
||||
}
|
||||
|
||||
impl PortRule {
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
match_all: 0,
|
||||
count: 0,
|
||||
_pad: [0; 2],
|
||||
ports: [0; MAX_RULES_PORT],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_match_all() -> Self {
|
||||
Self {
|
||||
match_all: PORT_RULE_MATCH_ALL,
|
||||
count: 0,
|
||||
_pad: [0; 2],
|
||||
ports: [0; MAX_RULES_PORT],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_match_all(&self) -> bool {
|
||||
self.match_all == PORT_RULE_MATCH_ALL
|
||||
}
|
||||
|
||||
pub fn contains(&self, port: Port) -> bool {
|
||||
if self.is_match_all() {
|
||||
return true;
|
||||
}
|
||||
for i in 0..(self.count as usize) {
|
||||
if i >= MAX_RULES_PORT {
|
||||
break;
|
||||
}
|
||||
if self.ports[i] == port {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub fn add_port(&mut self, port: Port) -> bool {
|
||||
if self.is_match_all() {
|
||||
return true;
|
||||
}
|
||||
for i in 0..(self.count as usize) {
|
||||
if i >= MAX_RULES_PORT {
|
||||
return false;
|
||||
}
|
||||
if self.ports[i] == port {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (self.count as usize) >= MAX_RULES_PORT {
|
||||
return false;
|
||||
}
|
||||
self.ports[self.count as usize] = port;
|
||||
self.count += 1;
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub fn remove_port(&mut self, port: Port) -> bool {
|
||||
for i in 0..(self.count as usize) {
|
||||
if i >= MAX_RULES_PORT {
|
||||
break;
|
||||
}
|
||||
if self.ports[i] == port {
|
||||
for j in i..(self.count as usize - 1) {
|
||||
self.ports[j] = self.ports[j + 1];
|
||||
}
|
||||
self.count -= 1;
|
||||
self.ports[self.count as usize] = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub fn to_port_vec(&self) -> Vec<Port> {
|
||||
let count = (self.count as usize).min(MAX_RULES_PORT);
|
||||
self.ports[..count].to_vec()
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.is_match_all() && self.count == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for PortRule {}
|
||||
@ -1,19 +1,19 @@
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IPv4PseudoHeader {
|
||||
pub source_ip: u32,
|
||||
pub destination_ip: u32,
|
||||
pub zeros: u8,
|
||||
pub protocol: u8,
|
||||
pub length: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IPv6PseudoHeader {
|
||||
pub source_ip: u128,
|
||||
pub destination_ip: u128,
|
||||
pub length: u16,
|
||||
pub zeros: u8,
|
||||
pub next_header: u8,
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IPv4PseudoHeader {
|
||||
pub source_ip: u32,
|
||||
pub destination_ip: u32,
|
||||
pub zeros: u8,
|
||||
pub protocol: u8,
|
||||
pub length: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IPv6PseudoHeader {
|
||||
pub source_ip: u128,
|
||||
pub destination_ip: u128,
|
||||
pub length: u16,
|
||||
pub zeros: u8,
|
||||
pub next_header: u8,
|
||||
}
|
||||
12
net-guardia-abi/src/model/rate_limit.rs
Normal file
@ -0,0 +1,12 @@
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RateState {
|
||||
pub count: u64,
|
||||
pub window_start: u64,
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for RateState {}
|
||||
15
net-guardia-cli/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "net-guardia-cli"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "net-guardia-cli"
|
||||
path = "src/main.rs"
|
||||
398
net-guardia-cli/src/main.rs
Normal file
@ -0,0 +1,398 @@
|
||||
use std::io::{self, Write};
|
||||
use std::mem;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::time::Duration;
|
||||
use std::{env, fs};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use reqwest::{Client, Method};
|
||||
use serde_json::Value;
|
||||
|
||||
const CSRF_HEADER: &str = "X-CSRF-Token";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "net-guardia-cli", about = "NetGuardia CLI management tool", version)]
|
||||
struct Cli {
|
||||
#[arg(long, default_value = "http://127.0.0.1:8080", global = true, help = "API base URL")]
|
||||
url: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
#[command(about = "System health + enforce mode")]
|
||||
Status,
|
||||
#[command(about = "ML engine status")]
|
||||
Ml,
|
||||
#[command(about = "Add IP to source blacklist")]
|
||||
Block { ip: String },
|
||||
#[command(about = "Remove IP from source blacklist")]
|
||||
Unblock { ip: String },
|
||||
#[command(about = "List ACL rules")]
|
||||
Rules {
|
||||
#[arg(long, default_value = "source")]
|
||||
direction: String,
|
||||
#[arg(long, default_value = "blacklist")]
|
||||
list_type: String,
|
||||
},
|
||||
#[command(about = "Generate security report")]
|
||||
Report,
|
||||
#[command(about = "Get or set enforce mode")]
|
||||
Mode {
|
||||
#[arg(help = "Set mode to monitor or enforce")]
|
||||
mode: Option<String>,
|
||||
},
|
||||
#[command(about = "Authenticate and save JWT")]
|
||||
Login,
|
||||
#[command(about = "List SOAR active blocks")]
|
||||
Blocks,
|
||||
#[command(about = "List SOAR playbooks")]
|
||||
Playbooks,
|
||||
#[command(about = "List SOAR execution history")]
|
||||
Executions,
|
||||
#[command(about = "API key management")]
|
||||
ApiKey {
|
||||
#[command(subcommand)]
|
||||
action: ApiKeyAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ApiKeyAction {
|
||||
#[command(about = "Generate a new API key")]
|
||||
Generate {
|
||||
#[arg(long, default_value = "default")]
|
||||
name: String,
|
||||
#[arg(long, default_value = "read_only")]
|
||||
level: String,
|
||||
},
|
||||
#[command(about = "List all API keys")]
|
||||
List,
|
||||
#[command(about = "Revoke an API key")]
|
||||
Revoke { id: i64 },
|
||||
}
|
||||
|
||||
struct ApiClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
token_path: PathBuf,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
fn new(base_url: String) -> Result<Self, String> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
let token_path = dirs_next().join("token");
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
base_url,
|
||||
token_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_token(&self) -> Option<String> {
|
||||
fs::read_to_string(&self.token_path).ok()
|
||||
}
|
||||
|
||||
fn save_token(&self, token: &str) -> Result<(), String> {
|
||||
if let Some(parent) = self.token_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("Failed to create token directory: {}", e))?;
|
||||
}
|
||||
fs::write(&self.token_path, token).map_err(|e| format!("Failed to save token: {}", e))
|
||||
}
|
||||
|
||||
async fn get(&self, path: &str) -> Result<Value, String> {
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
let mut req = self.client.get(&url);
|
||||
if let Some(token) = self.load_token() {
|
||||
req = req.header("Authorization", format!("Bearer {}", token.trim()));
|
||||
}
|
||||
let resp = req.send().await.map_err(|e| format!("Connection error: {}", e))?;
|
||||
let status = resp.status().as_u16();
|
||||
if status == 401 {
|
||||
return Err("Session expired. Run `net-guardia-cli login` to re-authenticate.".into());
|
||||
}
|
||||
let text = resp.text().await.map_err(|e| format!("Read error: {}", e))?;
|
||||
serde_json::from_str(&text).map_err(|_| {
|
||||
format!(
|
||||
"Unexpected response (HTTP {}): {}",
|
||||
status,
|
||||
&text[..text.len().min(200)]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn request(&self, method: Method, path: &str, body: Option<Value>) -> Result<Value, String> {
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
let include_csrf = should_send_csrf(&method);
|
||||
let mut req = self.client.request(method, &url);
|
||||
if let Some(token) = self.load_token() {
|
||||
req = req.header("Authorization", format!("Bearer {}", token.trim()));
|
||||
}
|
||||
if include_csrf {
|
||||
req = req.header(CSRF_HEADER, "net-guardia-cli");
|
||||
}
|
||||
if let Some(b) = body {
|
||||
req = req.json(&b);
|
||||
}
|
||||
let resp = req.send().await.map_err(|e| format!("Connection error: {}", e))?;
|
||||
let status = resp.status().as_u16();
|
||||
if status == 401 {
|
||||
return Err("Session expired. Run `net-guardia-cli login` to re-authenticate.".into());
|
||||
}
|
||||
let text = resp.text().await.map_err(|e| format!("Read error: {}", e))?;
|
||||
if text.is_empty() {
|
||||
if (200..300).contains(&status) {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
return Err(format!("Empty response (HTTP {})", status));
|
||||
}
|
||||
serde_json::from_str(&text).map_err(|_| {
|
||||
format!(
|
||||
"Unexpected response (HTTP {}): {}",
|
||||
status,
|
||||
&text[..text.len().min(200)]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn login(&self, username: &str, password: &str) -> Result<String, String> {
|
||||
let url = format!("{}/api/auth/login", self.base_url);
|
||||
let body = serde_json::json!({"username": username, "password": password});
|
||||
let resp = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Connection error: {}", e))?;
|
||||
let data: Value = resp.json().await.map_err(|e| format!("Parse error: {}", e))?;
|
||||
data.get("token")
|
||||
.and_then(|t| t.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
data.get("error")
|
||||
.and_then(|e| e.as_str())
|
||||
.unwrap_or("Login failed")
|
||||
.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn dirs_next() -> PathBuf {
|
||||
let home = env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||
PathBuf::from(home).join(".net-guardia-cli")
|
||||
}
|
||||
|
||||
fn print_json(data: &Value) {
|
||||
println!("{}", serde_json::to_string_pretty(data).unwrap_or_default());
|
||||
}
|
||||
|
||||
fn should_send_csrf(method: &Method) -> bool {
|
||||
!matches!(*method, Method::GET | Method::HEAD | Method::OPTIONS)
|
||||
}
|
||||
|
||||
fn read_password() -> Result<String, String> {
|
||||
let fd = io::stdin().as_raw_fd();
|
||||
let mut termios = unsafe { mem::zeroed::<libc::termios>() };
|
||||
unsafe { libc::tcgetattr(fd, &mut termios) };
|
||||
let old = termios;
|
||||
termios.c_lflag &= !libc::ECHO;
|
||||
unsafe { libc::tcsetattr(fd, libc::TCSANOW, &termios) };
|
||||
|
||||
let mut password = String::new();
|
||||
let read_result = io::stdin()
|
||||
.read_line(&mut password)
|
||||
.map_err(|e| format!("Failed to read password: {}", e));
|
||||
println!();
|
||||
|
||||
unsafe { libc::tcsetattr(fd, libc::TCSANOW, &old) };
|
||||
read_result.map(|_| password.trim().to_string())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
let api = ApiClient::new(cli.url).unwrap_or_else(|e| {
|
||||
eprintln!("Error: {}", e);
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::Status => api.get("/api/health/status").await.map(|d| print_json(&d)),
|
||||
Commands::Ml => api.get("/api/ml/status").await.map(|d| print_json(&d)),
|
||||
Commands::Block { ip } => {
|
||||
let is_v6 = ip.contains(':');
|
||||
let ip_ver = if is_v6 { "ipv6" } else { "ipv4" };
|
||||
let addr = if is_v6 {
|
||||
format!("[{}]:0", ip)
|
||||
} else {
|
||||
format!("{}:0", ip)
|
||||
};
|
||||
api.request(
|
||||
Method::PUT,
|
||||
&format!("/api/acl/{}/source/blacklist", ip_ver),
|
||||
Some(Value::String(addr)),
|
||||
)
|
||||
.await
|
||||
.map(|_| println!("Blocked: {}", ip))
|
||||
}
|
||||
Commands::Unblock { ip } => {
|
||||
let is_v6 = ip.contains(':');
|
||||
let ip_ver = if is_v6 { "ipv6" } else { "ipv4" };
|
||||
let addr = if is_v6 {
|
||||
format!("[{}]:0", ip)
|
||||
} else {
|
||||
format!("{}:0", ip)
|
||||
};
|
||||
api.request(
|
||||
Method::DELETE,
|
||||
&format!("/api/acl/{}/source/blacklist", ip_ver),
|
||||
Some(Value::String(addr)),
|
||||
)
|
||||
.await
|
||||
.map(|_| println!("Unblocked: {}", ip))
|
||||
}
|
||||
Commands::Rules { direction, list_type } => {
|
||||
let v4 = api.get(&format!("/api/acl/ipv4/{}/{}", direction, list_type)).await;
|
||||
let v6 = api.get(&format!("/api/acl/ipv6/{}/{}", direction, list_type)).await;
|
||||
println!("=== IPv4 {} {} ===", direction, list_type);
|
||||
match v4 {
|
||||
Ok(d) => print_json(&d),
|
||||
Err(e) => eprintln!("{}", e),
|
||||
}
|
||||
println!("\n=== IPv6 {} {} ===", direction, list_type);
|
||||
match v6 {
|
||||
Ok(d) => print_json(&d),
|
||||
Err(e) => eprintln!("{}", e),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Commands::Report => api.get("/api/report/data").await.map(|d| print_json(&d)),
|
||||
Commands::Mode { mode } => match mode {
|
||||
Some(m) => {
|
||||
let body = serde_json::json!({"mode": m});
|
||||
api.request(Method::PUT, "/api/system/enforce-mode", Some(body))
|
||||
.await
|
||||
.map(|d| print_json(&d))
|
||||
}
|
||||
None => api.get("/api/system/enforce-mode").await.map(|d| print_json(&d)),
|
||||
},
|
||||
Commands::Login => {
|
||||
print!("Username: ");
|
||||
let mut stdout = io::stdout();
|
||||
if let Err(e) = stdout.flush() {
|
||||
eprintln_and_exit(format!("Failed to flush stdout: {}", e));
|
||||
}
|
||||
let mut username = String::new();
|
||||
if let Err(e) = io::stdin().read_line(&mut username) {
|
||||
eprintln_and_exit(format!("Failed to read username: {}", e));
|
||||
}
|
||||
let username = username.trim();
|
||||
|
||||
print!("Password: ");
|
||||
if let Err(e) = stdout.flush() {
|
||||
eprintln_and_exit(format!("Failed to flush stdout: {}", e));
|
||||
}
|
||||
let password = match read_password() {
|
||||
Ok(password) => password,
|
||||
Err(e) => eprintln_and_exit(e),
|
||||
};
|
||||
|
||||
match api.login(username, &password).await {
|
||||
Ok(token) => match api.save_token(&token) {
|
||||
Ok(()) => {
|
||||
println!("Login successful. Token saved to ~/.net-guardia-cli/token");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
Commands::Blocks => api.get("/api/soar/blocks").await.map(|d| print_json(&d)),
|
||||
Commands::Playbooks => api.get("/api/soar/playbooks").await.map(|d| print_json(&d)),
|
||||
Commands::Executions => api.get("/api/soar/executions").await.map(|d| print_json(&d)),
|
||||
Commands::ApiKey { action } => match action {
|
||||
ApiKeyAction::Generate { name, level } => {
|
||||
let body = serde_json::json!({"name": name, "level": level});
|
||||
api.request(Method::POST, "/api/api-keys/generate", Some(body))
|
||||
.await
|
||||
.map(|data| {
|
||||
if let Some(key) = data.get("key").and_then(|k| k.as_str()) {
|
||||
println!("Generated API key: {}", key);
|
||||
println!("Name: {}, Level: {}", name, level);
|
||||
println!("Set NETGUARDIA_API_KEY={} in your client config", key);
|
||||
} else {
|
||||
print_json(&data);
|
||||
}
|
||||
})
|
||||
}
|
||||
ApiKeyAction::List => api.get("/api/api-keys").await.map(|data| {
|
||||
if let Some(keys) = data.as_array() {
|
||||
if keys.is_empty() {
|
||||
println!("No API keys found.");
|
||||
} else {
|
||||
println!("{:<6} {:<20} {:<15} {:<22} Last Used", "ID", "Name", "Level", "Created");
|
||||
println!("{}", "-".repeat(80));
|
||||
for key in keys {
|
||||
println!(
|
||||
"{:<6} {:<20} {:<15} {:<22} {}",
|
||||
key.get("id").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||
key.get("name").and_then(|v| v.as_str()).unwrap_or("-"),
|
||||
key.get("permission_level").and_then(|v| v.as_str()).unwrap_or("-"),
|
||||
key.get("created_at").and_then(|v| v.as_str()).unwrap_or("-"),
|
||||
key.get("last_used_at").and_then(|v| v.as_str()).unwrap_or("never"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print_json(&data);
|
||||
}
|
||||
}),
|
||||
ApiKeyAction::Revoke { id } => api
|
||||
.request(Method::DELETE, &format!("/api/api-keys/{}", id), None)
|
||||
.await
|
||||
.map(|data| {
|
||||
if data.get("deleted").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
println!("Key #{} revoked successfully.", id);
|
||||
} else {
|
||||
print_json(&data);
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("Error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn eprintln_and_exit(message: String) -> ! {
|
||||
eprintln!("Error: {}", message);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn csrf_header_is_only_needed_for_state_changing_methods() {
|
||||
assert!(!should_send_csrf(&Method::GET));
|
||||
assert!(!should_send_csrf(&Method::HEAD));
|
||||
assert!(!should_send_csrf(&Method::OPTIONS));
|
||||
assert!(should_send_csrf(&Method::POST));
|
||||
assert!(should_send_csrf(&Method::PUT));
|
||||
assert!(should_send_csrf(&Method::DELETE));
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
#![no_std]
|
||||
|
||||
pub mod model;
|
||||
|
||||
/// Maximum number of statistics entries that can be stored
|
||||
///
|
||||
/// This constant defines the upper limit for statistical data entries
|
||||
/// to prevent unbounded memory growth.
|
||||
pub const MAX_STATS: u32 = 100000;
|
||||
/// Maximum number of network filtering rules allowed
|
||||
///
|
||||
/// Limits the number of rules that can be configured to ensure
|
||||
/// predictable performance and resource usage.
|
||||
pub const MAX_RULES: u32 = 1000;
|
||||
/// Maximum number of port-specific rules allowed
|
||||
///
|
||||
/// Defines the upper limit for port-based filtering rules to maintain
|
||||
/// efficient rule processing.
|
||||
pub const MAX_RULES_PORT: usize = 32;
|
||||
|
||||
pub const MAX_PORT_ACCESS: usize = 12;
|
||||
@ -1,112 +0,0 @@
|
||||
use network_types::eth::EtherType;
|
||||
use crate::model::ip_address::{EbpfAddrPortV4, EbpfAddrPortV6};
|
||||
use network_types::ip::IpProto;
|
||||
|
||||
pub struct Event {
|
||||
pub eth_type: EtherType,
|
||||
pub protocol: IpProto,
|
||||
pub source_ip: u128,
|
||||
pub destination_ip: u128,
|
||||
pub source_port: u16,
|
||||
pub destination_port: u16,
|
||||
pub len: u32,
|
||||
pub timestamp: u64
|
||||
}
|
||||
|
||||
impl Event {
|
||||
#[inline(always)]
|
||||
pub fn to_ipv4_event(&self) -> IPv4Event {
|
||||
IPv4Event {
|
||||
protocol: self.protocol,
|
||||
source_ip: self.source_ip as u32,
|
||||
destination_ip: self.destination_ip as u32,
|
||||
source_port: self.source_port,
|
||||
destination_port: self.destination_port,
|
||||
len: self.len,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_ipv6_event(&self) -> IPv6Event {
|
||||
IPv6Event {
|
||||
protocol: self.protocol,
|
||||
source_ip: self.source_ip,
|
||||
destination_ip: self.destination_ip,
|
||||
source_port: self.source_port,
|
||||
destination_port: self.destination_port,
|
||||
len: self.len,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_ipv4_event(self) -> IPv4Event {
|
||||
IPv4Event {
|
||||
protocol: self.protocol,
|
||||
source_ip: self.source_ip as u32,
|
||||
destination_ip: self.destination_ip as u32,
|
||||
source_port: self.source_port,
|
||||
destination_port: self.destination_port,
|
||||
len: self.len,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_ipv6_event(self) -> IPv6Event {
|
||||
IPv6Event {
|
||||
protocol: self.protocol,
|
||||
source_ip: self.source_ip,
|
||||
destination_ip: self.destination_ip,
|
||||
source_port: self.source_port,
|
||||
destination_port: self.destination_port,
|
||||
len: self.len,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IPv4Event {
|
||||
pub protocol: IpProto,
|
||||
pub source_ip: u32,
|
||||
pub destination_ip: u32,
|
||||
pub source_port: u16,
|
||||
pub destination_port: u16,
|
||||
pub len: u32,
|
||||
pub timestamp: u64
|
||||
}
|
||||
|
||||
impl IPv4Event {
|
||||
#[inline(always)]
|
||||
pub fn get_source(&self) -> EbpfAddrPortV4 {
|
||||
[self.source_ip, self.source_port as u32]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_destination(&self) -> EbpfAddrPortV4 {
|
||||
[self.destination_ip, self.destination_port as u32]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IPv6Event {
|
||||
pub protocol: IpProto,
|
||||
pub source_ip: u128,
|
||||
pub destination_ip: u128,
|
||||
pub source_port: u16,
|
||||
pub destination_port: u16,
|
||||
pub len: u32,
|
||||
pub timestamp: u64
|
||||
}
|
||||
|
||||
impl IPv6Event {
|
||||
#[inline(always)]
|
||||
pub fn get_source(&self) -> EbpfAddrPortV6 {
|
||||
[self.source_ip, self.source_port as u128]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_destination(&self) -> EbpfAddrPortV6 {
|
||||
[self.destination_ip, self.destination_port as u128]
|
||||
}
|
||||
}
|
||||