mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
feat: symmetric hash, drop events, GeoIP/DNS, performance & bug fixes (#15)
* fix: ML formula alignment, dependency cleanup, and code quality - Restore correct MSE denominator (ae_feature_names.len()) - Align feature extraction with CICFlowMeter: payload bytes, sample std (N-1), min_seg_size_forward without payload filter, act_data_pkt_fwd skip first packet, init_win_bytes_bwd stores last packet - Organize workspace dependencies and update all crates to latest - Fix libxdp-sys 0.2.4 clang 20 build (enable use_cc_build + use_precompiled_bpf) - Pin aya-ebpf =0.1.1 (0.1.2 yanked, aya-rs/aya#1400) - Remove all comments and dead code - Use macro-generated constructors for error types - Replace eprintln/tracing::error with log! macro - Clean up duplicate/unused error and log variants, fix log levels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct UDP/TCP/mid-stream forward/backward direction logic * fix: symmetric hash redirect, per-queue optimization, and ICMP ACL support eBPF: Add XOR symmetric hash to transmission stage so both directions of a flow land on the same XSK queue, fixing the RSS asymmetry bug that caused FlowTracker to only see one direction. Egress uses stack-local ParsedPacket to avoid unnecessary map allocation. Allow non-TCP/UDP packets through the pipeline so ACL can block ICMP. Userspace: Revert shared tracker to per-queue trackers now that symmetric hash guarantees bidirectional visibility. Replace Arc<Mutex<Vec<FrameDesc>>> with private Vec per XskPair to eliminate hot-path lock contention. Add pre-allocated BufferPool and parse-before-clone to reduce per-packet malloc. Log partial sends when frames < packets. Config: Add mtu, packet_buffer_size, buffer_pool_capacity to [Network] with serde defaults for backward compatibility. Cleanup: Remove unused Direction::flip(), FlowTracker::drain_flows(), FlowStatistics broadcast/subscribe, MAX_BUFFERED_PACKETS, and Engine::process_packet wrapper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: drop event ring buffer, double-buffer flow tracker, and review fixes eBPF: Add 256KB RingBuf map for drop event reporting. Each XDP_DROP site (ACL blacklist, rate limit, protocol filter) emits a DropEvent with reason code. Refactor rate_limit::should_drop to return Option<u8> with specific reason. Extract rate limit defaults to common/define/setting. Userspace: Add DropMonitor with tokio async consumer, broadcast channel, DropCounters, and /api/stats/drops REST + /ws/drops WebSocket endpoints. FlowTracker: Replace O(flows) lock with O(1) take_snapshot() swap. Inference tick now does phase-1 swap under lock, phase-2 filter outside lock. Removes cleanup_old_flows, get_flows_for_inference, drain_flows_for_logging. Review fixes: explicit IPv6 match in symmetric_hash, consistent naming (compute_symmetric_queue_id), reason_to_str helper, std::net::Ipv6Addr formatting, WebSocketLagged typo, remove unused mtu config field. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move IP field writes before L4 match to prevent eBPF dead store elimination The eBPF LLVM backend was optimizing away the non-TCP/UDP path by treating ParsedPacket writes as dead stores (reads happen in separate tail-called programs). Moving IP-level fields (timestamp, src/dst IP, packet_length, ip_version, protocol) before the L4 protocol match ensures they are written regardless of protocol, sharing code path with TCP/UDP and preventing branch merging with the error path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: GeoIP LPM Trie country blocking and DNS query blacklist eBPF: Add LPM Trie maps (GEO_BLOCK_V4/V6) in access_control for country-level IP blocking with big-endian keys for correct prefix matching. Add DNS query name parser in dns_filter with bounded loops, lowercase normalization, and subdomain matching via parent domain iteration. Both emit drop events with new reason codes. Userspace: Add GeoBlock manager that loads CIDR prefixes from MaxMind GeoLite2 database into LPM Tries per blocked country. Add DnsFilter manager with wire-format domain conversion. New REST endpoints: PUT/DELETE/GET /api/acl/geo/{block,unblock,blocked} and PUT/DELETE/GET /api/filter/dns/blacklist. Review fixes: DNS header bounds check off-by-one (+2 to +3), total_prefixes accumulation (= to +=), proper InvalidDnsName error type replacing InvalidMapType abuse. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: review fixes for GeoIP and DNS features GeoIP: Fix LPM Trie endianness (from_be_bytes → from_ne_bytes for consistent memory layout). Move whitelist check before geo/blacklist (whitelist should always take priority). Batch block/unblock API to rebuild tries only once. Minimize rebuild gap by collecting entries before locking. Use config path for GeoIP DB. Validate country codes (2-letter alpha). DNS: Fix bounds check off-by-one (dns_header+2 → +3). Add QDCOUNT>0 check. Replace InvalidMapType error with proper InvalidDnsName variant. Add domain count limit per request (1000). Wrap API responses in JSON objects for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add deployment and testing infrastructure Add container definitions, compose config, and traffic generator scripts for realistic inline deployment testing. deploy/compose/ — Containerfiles for netguardia, router, endpoints and podman-compose.yml with management network deploy/scripts/ — setup.sh (veth/namespace wiring), traffic-external.sh and traffic-internal.sh Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: P0 bugs — IPv4 IHL parsing, TX frame leak, SSH whitelist logic IPv4 IHL: L4 header offset was hardcoded to 34 (20-byte IPv4 header). Now reads IHL field dynamically (20-60 bytes) so IP options don't cause wrong port/flag parsing. Attackers could previously bypass port-based rules by adding IP options. TX frame leak: tx.produce() may submit fewer frames than provided. Unsubmitted FrameDescs were lost, permanently shrinking frame_pool. Now returns unsubmitted frames to pool. SSH whitelist: Array<PlaceHolder>.get(0).is_some() always returns true (zero-initialized entries exist). Changed to check actual value != 0, matching userspace enable(1)/disable(0) semantics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: P1 bugs — rate_limit real values, ML status, DNS rate protocol check rate_limit API: GET /api/rate-limit/config now reads actual values from eBPF Array map instead of returning hardcoded defaults. Added getter methods for each rate limit parameter. ML status API: GET /api/ml/status now returns real engine state (mode, tracker count, flow count, inference interval) instead of hardcoded {"active": true}. DNS rate limit: Added UDP protocol check before dst_port==53 test. TCP connections to port 53 no longer incorrectly trigger DNS rate limiter alongside packet rate limiter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf: P2 fixes — binary IPs, pre-alloc buffers, build.rs dedup parse_packet: Replace String IP addresses with [u8; 16] binary in UserPacket, eliminating 2M heap allocations/sec at 1Mpps. FlowKey copies bytes directly instead of string-parse-to-bytes round-trip. xsk_manager: Pre-allocate comp_descs (256) and rx_descs (64) once before the main loop instead of per-iteration vec![] allocation. build.rs: Extract duplicate build_ingress_ebpf/build_egress_ebpf into shared build_ebpf_package(). Add cargo:rerun-if-changed for common/src to fix stale eBPF build cache when common crate changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf: P2 fixes — feature_extractor precompute, filter.rs dedup feature_extractor: Pre-compute all statistics once via PrecomputedStats struct instead of recomputing per feature name. ~70x speedup for the 72-feature extraction step. Public API unchanged. filter.rs: Extract ok_or_error() helper to eliminate 16 instances of duplicated match-result-to-HttpResponse pattern. File reduced from 338 to 243 lines with identical behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: P3 code quality — release profile, dep cleanup, dead code, safety Release profile: Enable opt-level=3, lto="thin", strip=true. Dependencies: Remove unused `futures` crate. Change tokio from "full" to selective features (rt-multi-thread, macros, sync, time). Remove commented-out csv/anyhow deps. Dead code: Remove unused get_attack_label(), unnecessary #[allow(dead_code)] annotations where code is actually used. Safety: Add bounds check in PortRule::to_port_vec to prevent OOB if count is corrupted. Fix set_config to propagate errors instead of silently ignoring with `let _ =`. Misc: Add .env, .DS_Store, profiling files to .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: ParrotXray <b05817691@gmail.com>
This commit is contained in:
parent
18a2d4a168
commit
ebc7edcead
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@ -13,4 +13,16 @@ logs
|
||||
TODO
|
||||
.log
|
||||
.txt
|
||||
.csv
|
||||
net-guardia/static/web
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Profiling
|
||||
*.profraw
|
||||
*.profdata
|
||||
|
||||
1053
Cargo.lock
generated
1053
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
64
Cargo.toml
64
Cargo.toml
@ -4,26 +4,70 @@ members = ["net-guardia", "common", "macros", "ingress-ebpf", "egress-ebpf"]
|
||||
default-members = ["net-guardia", "common"]
|
||||
|
||||
[workspace.dependencies]
|
||||
# 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-ebpf = { version = "0.1.1", default-features = false }
|
||||
aya-log = { version = "0.2.1", default-features = false }
|
||||
aya-log-ebpf = { version = "0.1.0", default-features = false }
|
||||
cargo_metadata = { version = "0.23.1", default-features = false }
|
||||
libc = { version = "0.2.159", default-features = false }
|
||||
network-types = "0.1.0"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
|
||||
# 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"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
||||
|
||||
# Web framework
|
||||
actix = "0.13.5"
|
||||
actix-web = "4.13.0"
|
||||
actix-cors = "0.7.1"
|
||||
actix-ws = "0.4.0"
|
||||
|
||||
# Logging / tracing
|
||||
tracing = "0.1.44"
|
||||
tracing-appender = "0.2.4"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
|
||||
# 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.38.4"
|
||||
maxminddb = "0.27.3"
|
||||
ipnetwork = "0.21.1"
|
||||
lru = "0.16.3"
|
||||
|
||||
# Build dependencies
|
||||
cargo_metadata = { version = "0.23.1", default-features = false }
|
||||
which = "8.0.2"
|
||||
|
||||
# Proc macro
|
||||
proc-macro2 = "1.0.106"
|
||||
quote = "1.0.45"
|
||||
syn = { version = "2.0.117", features = ["full"] }
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
#opt-level = 3
|
||||
#lto = true
|
||||
#strip = true
|
||||
#debug = false
|
||||
#overflow-checks = false
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
strip = true
|
||||
|
||||
[profile.release.package.ingress-ebpf]
|
||||
debug = 2
|
||||
|
||||
674
LICENSE
674
LICENSE
@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
8
common/src/define/drop_reason.rs
Normal file
8
common/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;
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod drop_reason;
|
||||
pub mod offset;
|
||||
pub mod other;
|
||||
pub mod pipeline;
|
||||
pub mod rate_limit;
|
||||
pub mod setting;
|
||||
pub mod tcp_flags;
|
||||
|
||||
@ -1 +0,0 @@
|
||||
pub const STANDARD_MTU: usize = 1500;
|
||||
5
common/src/define/rate_limit.rs
Normal file
5
common/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;
|
||||
@ -1,4 +1,10 @@
|
||||
pub const MAX_STATS: usize = 131072;
|
||||
pub const MAX_RULES: usize = 128;
|
||||
pub const MAX_RULES_PORT: usize = 32;
|
||||
pub const MAX_BUFFERED_PACKETS: usize = 1024;
|
||||
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;
|
||||
|
||||
@ -1 +1,2 @@
|
||||
pub mod parsing;
|
||||
pub mod symmetric_hash;
|
||||
|
||||
@ -31,22 +31,23 @@ unsafe fn parse_ipv4_packet(start: usize, end: usize, target: *mut ParsedPacket)
|
||||
let ipv4 = &*((start + IPV4_HEADER_START) as *const Ipv4Hdr);
|
||||
let packet_length = (end - start) as u32;
|
||||
|
||||
let (src_port, dst_port, tcp_flags, l4_header_len) = match ipv4.proto {
|
||||
IpProto::Tcp => parse_tcp(start, end, IPV4_TCP_HEADER_START, IPV4_TCP_HEADER_END)?,
|
||||
IpProto::Udp => parse_udp(start, end, IPV4_UDP_HEADER_START, IPV4_UDP_HEADER_END)?,
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
let t = &mut *target;
|
||||
t.timestamp_ns = bpf_ktime_get_ns();
|
||||
core::ptr::copy_nonoverlapping(ipv4.src_addr.as_ptr(), t.src_ip.as_mut_ptr(), 4);
|
||||
core::ptr::copy_nonoverlapping(ipv4.dst_addr.as_ptr(), t.dst_ip.as_mut_ptr(), 4);
|
||||
t.packet_length = packet_length;
|
||||
t.ip_version = 4;
|
||||
t.protocol = ipv4.proto;
|
||||
|
||||
let (src_port, dst_port, tcp_flags, l4_header_len) = match ipv4.proto {
|
||||
IpProto::Tcp => parse_tcp(start, end, IPV4_TCP_HEADER_START, IPV4_TCP_HEADER_END)?,
|
||||
IpProto::Udp => parse_udp(start, end, IPV4_UDP_HEADER_START, IPV4_UDP_HEADER_END)?,
|
||||
_ => (0, 0, 0, 0),
|
||||
};
|
||||
|
||||
t.payload_length = packet_length.saturating_sub((IPV4_HEADER_END + l4_header_len) as u32);
|
||||
t.src_port = src_port;
|
||||
t.dst_port = dst_port;
|
||||
t.ip_version = 4;
|
||||
t.protocol = ipv4.proto;
|
||||
t.tcp_flags = tcp_flags;
|
||||
|
||||
Ok(())
|
||||
@ -61,22 +62,23 @@ unsafe fn parse_ipv6_packet(start: usize, end: usize, target: *mut ParsedPacket)
|
||||
let ipv6 = &*((start + IPV6_HEADER_START) as *const Ipv6Hdr);
|
||||
let packet_length = (end - start) as u32;
|
||||
|
||||
let (src_port, dst_port, tcp_flags, l4_header_len) = match ipv6.next_hdr {
|
||||
IpProto::Tcp => parse_tcp(start, end, IPV6_TCP_HEADER_START, IPV6_TCP_HEADER_END)?,
|
||||
IpProto::Udp => parse_udp(start, end, IPV6_UDP_HEADER_START, IPV6_UDP_HEADER_END)?,
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
let t = &mut *target;
|
||||
t.timestamp_ns = bpf_ktime_get_ns();
|
||||
core::ptr::copy_nonoverlapping(ipv6.src_addr.as_ptr(), t.src_ip.as_mut_ptr(), 16);
|
||||
core::ptr::copy_nonoverlapping(ipv6.dst_addr.as_ptr(), t.dst_ip.as_mut_ptr(), 16);
|
||||
t.packet_length = packet_length;
|
||||
t.ip_version = 6;
|
||||
t.protocol = ipv6.next_hdr;
|
||||
|
||||
let (src_port, dst_port, tcp_flags, l4_header_len) = match ipv6.next_hdr {
|
||||
IpProto::Tcp => parse_tcp(start, end, IPV6_TCP_HEADER_START, IPV6_TCP_HEADER_END)?,
|
||||
IpProto::Udp => parse_udp(start, end, IPV6_UDP_HEADER_START, IPV6_UDP_HEADER_END)?,
|
||||
_ => (0, 0, 0, 0),
|
||||
};
|
||||
|
||||
t.payload_length = packet_length.saturating_sub((IPV6_HEADER_END + l4_header_len) as u32);
|
||||
t.src_port = src_port;
|
||||
t.dst_port = dst_port;
|
||||
t.ip_version = 6;
|
||||
t.protocol = ipv6.next_hdr;
|
||||
t.tcp_flags = tcp_flags;
|
||||
|
||||
Ok(())
|
||||
|
||||
24
common/src/ebpf/symmetric_hash.rs
Normal file
24
common/src/ebpf/symmetric_hash.rs
Normal file
@ -0,0 +1,24 @@
|
||||
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 {
|
||||
4 => pkt.src_ip_v4() ^ pkt.dst_ip_v4(),
|
||||
6 => {
|
||||
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 u8 as u32)).wrapping_mul(2654435761);
|
||||
|
||||
Some(h % num_queues)
|
||||
}
|
||||
19
common/src/model/dns_name.rs
Normal file
19
common/src/model/dns_name.rs
Normal file
@ -0,0 +1,19 @@
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
|
||||
/// Fixed-size DNS name in wire format (length-prefixed labels).
|
||||
/// Stored lowercase, zero-padded. Example: \x07example\x03com\x00
|
||||
#[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
common/src/model/drop_event.rs
Normal file
13
common/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,3 +1,5 @@
|
||||
pub mod dns_name;
|
||||
pub mod drop_event;
|
||||
pub mod flow_stats;
|
||||
pub mod http_method;
|
||||
pub mod ip_address;
|
||||
|
||||
@ -65,11 +65,11 @@ impl PortRule {
|
||||
return false;
|
||||
}
|
||||
if self.ports[i] == port {
|
||||
return true; // already exists
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (self.count as usize) >= MAX_RULES_PORT {
|
||||
return false; // full
|
||||
return false;
|
||||
}
|
||||
self.ports[self.count as usize] = port;
|
||||
self.count += 1;
|
||||
@ -83,7 +83,6 @@ impl PortRule {
|
||||
break;
|
||||
}
|
||||
if self.ports[i] == port {
|
||||
// shift remaining
|
||||
for j in i..(self.count as usize - 1) {
|
||||
self.ports[j] = self.ports[j + 1];
|
||||
}
|
||||
@ -97,7 +96,8 @@ impl PortRule {
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub fn to_port_vec(&self) -> Vec<Port> {
|
||||
self.ports[..self.count as usize].to_vec()
|
||||
let count = (self.count as usize).min(MAX_RULES_PORT);
|
||||
self.ports[..count].to_vec()
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
|
||||
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;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RateState {
|
||||
|
||||
@ -27,7 +27,7 @@ traffic_logging_mode = true
|
||||
traffic_log_csv_path = "traffic_log.csv"
|
||||
|
||||
[Misc]
|
||||
geoip_db_name = "GeoLite2-City.mmdb"
|
||||
geoip_db_name = "net-guardia/static/geo/GeoLite2-City.mmdb"
|
||||
|
||||
[Pipeline]
|
||||
ingress = ["access_control", "rate_limit", "service"]
|
||||
|
||||
25
deploy/compose/Containerfile.endpoint
Normal file
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"]
|
||||
46
deploy/compose/Containerfile.netguardia
Normal file
46
deploy/compose/Containerfile.netguardia
Normal file
@ -0,0 +1,46 @@
|
||||
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 \
|
||||
openssh-server \
|
||||
ethtool \
|
||||
nodejs24 \
|
||||
nodejs24-npm \
|
||||
m4 \
|
||||
make pkg-config \
|
||||
&& dnf clean all
|
||||
|
||||
# Rust toolchain
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
&& /root/.cargo/bin/rustup toolchain install nightly \
|
||||
&& /root/.cargo/bin/rustup component add rust-src --toolchain nightly
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# bpf-linker for aya eBPF compilation
|
||||
RUN cargo install bpf-linker
|
||||
|
||||
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
|
||||
|
||||
RUN echo 'root:REDACTED' | chpasswd && \
|
||||
sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
||||
sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
||||
|
||||
WORKDIR /root/NetGuardia
|
||||
CMD sh -c "ssh-keygen -A && /usr/sbin/sshd && sleep infinity"
|
||||
10
deploy/compose/Containerfile.router
Normal file
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"]
|
||||
72
deploy/compose/podman-compose.yml
Normal file
72
deploy/compose/podman-compose.yml
Normal file
@ -0,0 +1,72 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
netguardia:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: compose/Containerfile.netguardia
|
||||
container_name: netguardia
|
||||
hostname: netguardia
|
||||
privileged: true
|
||||
security_opt:
|
||||
- seccomp=unconfined
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
dns:
|
||||
- 10.10.3.1
|
||||
- 8.8.8.8
|
||||
networks:
|
||||
mgmt-net:
|
||||
ipv4_address: 10.10.3.10
|
||||
ports:
|
||||
- "2222:22"
|
||||
- "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
|
||||
container_name: router
|
||||
hostname: router
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
network_mode: none
|
||||
|
||||
external:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: compose/Containerfile.endpoint
|
||||
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
|
||||
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
|
||||
174
deploy/scripts/setup.sh
Normal file
174
deploy/scripts/setup.sh
Normal file
@ -0,0 +1,174 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if command -v podman-compose &>/dev/null; then
|
||||
COMPOSE="podman-compose"
|
||||
RT="podman"
|
||||
elif command -v docker &>/dev/null && docker compose version &>/dev/null 2>&1; then
|
||||
COMPOSE="docker compose"
|
||||
RT="docker"
|
||||
else
|
||||
echo "ERROR: No container runtime found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Runtime: $RT ==="
|
||||
echo "=== Kernel: $(uname -r) ==="
|
||||
echo ""
|
||||
|
||||
echo "=== Building containers ==="
|
||||
$COMPOSE build
|
||||
|
||||
echo "=== Starting containers ==="
|
||||
$COMPOSE up -d
|
||||
|
||||
echo ""
|
||||
echo "=== Containers running ==="
|
||||
$RT ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null || $RT ps
|
||||
|
||||
get_pid() {
|
||||
$RT inspect --format '{{.State.Pid}}' "$1"
|
||||
}
|
||||
|
||||
mkdir -p /var/run/netns
|
||||
|
||||
EXT_PID=$(get_pid external)
|
||||
INT_PID=$(get_pid internal)
|
||||
RTR_PID=$(get_pid router)
|
||||
NG_PID=$(get_pid netguardia)
|
||||
ln -sf /proc/$EXT_PID/ns/net /var/run/netns/external
|
||||
ln -sf /proc/$INT_PID/ns/net /var/run/netns/internal
|
||||
ln -sf /proc/$RTR_PID/ns/net /var/run/netns/router
|
||||
ln -sf /proc/$NG_PID/ns/net /var/run/netns/netguardia
|
||||
|
||||
# ============================================================
|
||||
# Segment 1: external <-> router (10.10.1.0/24)
|
||||
# Direct connection, no inspection needed
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "=== Segment 1: external <-> router (10.10.1.0/24) ==="
|
||||
|
||||
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
|
||||
|
||||
ip netns exec external ip link set lo up
|
||||
ip netns exec external ip link set ext-eth0 up
|
||||
ip netns exec external ip addr add 10.10.1.2/24 dev ext-eth0
|
||||
for i in 3 4 5 6 7; do
|
||||
ip netns exec external ip addr add 10.10.1.${i}/24 dev ext-eth0
|
||||
done
|
||||
ip netns exec external ip route add default via 10.10.1.1
|
||||
|
||||
ip netns exec router ip link set lo up
|
||||
ip netns exec router ip link set rtr-ext up
|
||||
ip netns exec router ip addr add 10.10.1.1/24 dev rtr-ext
|
||||
|
||||
echo " external: ext-eth0 10.10.1.{2-7}/24, gw 10.10.1.1"
|
||||
echo " router: rtr-ext 10.10.1.1/24"
|
||||
|
||||
# ============================================================
|
||||
# Segment 2: router <-> netguardia <-> internal (10.10.2.0/24)
|
||||
# NetGuardia inline: XDP on ng-ext (router side) and ng-int (internal side)
|
||||
# No bridges, no inline veth pair — direct XSK forwarding
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "=== Segment 2: router <-> [NetGuardia] <-> internal (10.10.2.0/24) ==="
|
||||
|
||||
# router <-> netguardia: ng-ext is the netguardia side
|
||||
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
|
||||
|
||||
# netguardia <-> internal: ng-int is the netguardia side
|
||||
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
|
||||
|
||||
# Router internal side
|
||||
ip netns exec router ip link set rtr-int up
|
||||
ip netns exec router ip addr add 10.10.2.1/24 dev rtr-int
|
||||
ip netns exec router sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
|
||||
|
||||
# Internal container
|
||||
ip netns exec internal ip link set lo up
|
||||
ip netns exec internal ip link set int-eth0 up
|
||||
ip netns exec internal ip addr add 10.10.2.2/24 dev int-eth0
|
||||
for i in 3 4 5 6; do
|
||||
ip netns exec internal ip addr add 10.10.2.${i}/24 dev int-eth0
|
||||
done
|
||||
ip netns exec internal ip route add default via 10.10.2.1
|
||||
|
||||
# NetGuardia interfaces (no IP, transparent)
|
||||
ip netns exec netguardia ip link set ng-ext up
|
||||
ip netns exec netguardia ip link set ng-int up
|
||||
|
||||
# Disable checksum offload on ALL veth endpoints.
|
||||
# AF_XDP TX bypasses the kernel stack, so checksums are not computed.
|
||||
# Without this, TCP packets forwarded through XSK have bad checksums and get dropped.
|
||||
ip netns exec router ethtool -K rtr-int tx off rx off 2>/dev/null || true
|
||||
ip netns exec router ethtool -K rtr-ext tx off rx off 2>/dev/null || true
|
||||
ip netns exec internal ethtool -K int-eth0 tx off rx off 2>/dev/null || true
|
||||
ip netns exec external ethtool -K ext-eth0 tx off rx off 2>/dev/null || true
|
||||
ip netns exec netguardia ethtool -K ng-ext tx off rx off 2>/dev/null || true
|
||||
ip netns exec netguardia ethtool -K ng-int tx off rx off 2>/dev/null || true
|
||||
|
||||
echo " router: rtr-int (10.10.2.1) <-> ng-ext (XDP ingress)"
|
||||
echo " netguardia: ng-ext <-> [XSK forwarding] <-> ng-int"
|
||||
echo " internal: int-eth0 (10.10.2.{2-6}) <-> ng-int (XDP egress)"
|
||||
echo " checksum offload disabled on all veth endpoints"
|
||||
|
||||
# ============================================================
|
||||
# Verify
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "=== Interfaces inside netguardia ==="
|
||||
ip netns exec netguardia ip -br link show
|
||||
|
||||
echo ""
|
||||
echo "=== Testing connectivity ==="
|
||||
|
||||
echo -n " external -> router: "
|
||||
ip netns exec external ping -c 1 -W 2 10.10.1.1 >/dev/null 2>&1 && echo "OK" || echo "FAIL"
|
||||
|
||||
# Without net-guardia, traffic between router and internal won't pass
|
||||
# because ng-ext/ng-int are just veth endpoints with no forwarding
|
||||
echo -n " router -> internal: "
|
||||
ip netns exec router ping -c 1 -W 2 10.10.2.2 >/dev/null 2>&1 && echo "OK" || echo "FAIL (expected - needs net-guardia)"
|
||||
|
||||
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 cp /tmp/netguardia_interfaces.txt netguardia:/root/NetGuardia/interfaces.txt 2>/dev/null || true
|
||||
|
||||
rm -f /var/run/netns/external /var/run/netns/internal /var/run/netns/router /var/run/netns/netguardia
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " NetGuardia realistic inline deployment!"
|
||||
echo ""
|
||||
echo " external (10.10.1.{2-7})"
|
||||
echo " |"
|
||||
echo " [router] 10.10.1.1 <-> 10.10.2.1"
|
||||
echo " | rtr-int"
|
||||
echo " |"
|
||||
echo " ng-ext (no IP) <- XDP ingress"
|
||||
echo " |"
|
||||
echo " [net-guardia XSK]"
|
||||
echo " |"
|
||||
echo " ng-int (no IP) <- XDP egress"
|
||||
echo " |"
|
||||
echo " | int-eth0"
|
||||
echo " internal (10.10.2.{2-6})"
|
||||
echo ""
|
||||
echo " All 10.10.2.0/24 traffic requires net-guardia!"
|
||||
echo " Mgmt: 10.10.3.10"
|
||||
echo " SSH: ssh -p 2222 root@<host-ip>"
|
||||
echo " Web: http://<host-ip>:8080"
|
||||
echo "=========================================="
|
||||
127
deploy/scripts/traffic-external.sh
Normal file
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
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,12 +5,12 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common", features = ["kernel"] }
|
||||
|
||||
aya-ebpf = { workspace = true }
|
||||
aya-log-ebpf = { workspace = true }
|
||||
network-types = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
which = "8.0.0"
|
||||
which = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "net-guardia-egress"
|
||||
|
||||
@ -3,23 +3,39 @@
|
||||
|
||||
use aya_ebpf::bindings::xdp_action;
|
||||
use aya_ebpf::macros::{map, xdp};
|
||||
use aya_ebpf::maps::XskMap;
|
||||
use aya_ebpf::maps::{Array, XskMap};
|
||||
use aya_ebpf::programs::XdpContext;
|
||||
#[allow(unused_imports)]
|
||||
use aya_log_ebpf::info;
|
||||
use common::ebpf::parsing;
|
||||
use common::ebpf::symmetric_hash::symmetric_queue_id;
|
||||
use common::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 { (*ctx.ctx).rx_queue_index };
|
||||
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> {
|
||||
let mut pkt = core::mem::zeroed::<ParsedPacket>();
|
||||
parsing::parse_packet(ctx.data(), ctx.data_end(), &mut pkt).ok()?;
|
||||
let num_q = *NUM_QUEUES.get(0)?;
|
||||
symmetric_queue_id(&pkt, num_q)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
|
||||
@ -5,17 +5,16 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common", features = ["kernel"] }
|
||||
|
||||
aya-ebpf = { workspace = true }
|
||||
aya-log-ebpf = { workspace = true }
|
||||
network-types = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
which = "8.0.0"
|
||||
which = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "net-guardia-ingress"
|
||||
path = "src/main.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
bench = false
|
||||
bench = false
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::HashMap;
|
||||
use common::define::setting::MAX_RULES;
|
||||
use aya_ebpf::maps::LpmTrie;
|
||||
use aya_ebpf::maps::lpm_trie::Key;
|
||||
use common::define::setting::{MAX_RULES, MAX_GEO_ENTRIES};
|
||||
use common::model::parsed_packet::ParsedPacket;
|
||||
use common::model::ip_address::{IPv4, IPv6};
|
||||
use common::model::port_rule::PortRule;
|
||||
@ -21,6 +23,24 @@ static IPV6_SRC_BLACKLIST: HashMap<IPv6, PortRule> = HashMap::with_max_entries(M
|
||||
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 {
|
||||
// from_ne_bytes so memory layout = raw packet bytes (network order).
|
||||
// Matches userspace insertion which uses to_bits().to_be() (same memory layout).
|
||||
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);
|
||||
unsafe { 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);
|
||||
unsafe { GEO_BLOCK_V6.get(&key).is_some() }
|
||||
}
|
||||
|
||||
pub fn ipv4_is_whitelisted(pkt: &ParsedPacket) -> bool {
|
||||
let src_ip = pkt.src_ip_v4();
|
||||
|
||||
@ -60,7 +60,6 @@ fn http_service_violation<K>(
|
||||
if pkt.tcp_flags & (TCP_PSH | TCP_ACK) != (TCP_PSH | TCP_ACK) {
|
||||
return false;
|
||||
}
|
||||
// Bounds check before reading IHL — verifier needs to see this
|
||||
if start + 15 > end {
|
||||
return false;
|
||||
}
|
||||
@ -110,7 +109,7 @@ fn get_http_request_method(start: usize, end: usize, offset: usize) -> Option<Ht
|
||||
fn ipv4_ssh_service_violation(source: &AddrPortV4, destination: &AddrPortV4) -> bool {
|
||||
unsafe {
|
||||
if IPV4_SSH_SERVICE.get(destination).is_some() {
|
||||
if SSH_WHITE_LIST_ENABLE.get(0).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()
|
||||
@ -125,7 +124,7 @@ fn ipv4_ssh_service_violation(source: &AddrPortV4, destination: &AddrPortV4) ->
|
||||
fn ipv6_ssh_service_violation(source: &AddrPortV6, destination: &AddrPortV6) -> bool {
|
||||
unsafe {
|
||||
if IPV6_SSH_SERVICE.get(destination).is_some() {
|
||||
if SSH_WHITE_LIST_ENABLE.get(0).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()
|
||||
|
||||
@ -1,21 +1,17 @@
|
||||
use aya_ebpf::helpers::bpf_ktime_get_ns;
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::{Array, LruHashMap};
|
||||
use common::define::drop_reason::*;
|
||||
use common::define::rate_limit::*;
|
||||
use common::define::setting::*;
|
||||
use common::define::tcp_flags::*;
|
||||
use common::model::ip_address::{IPv4, IPv6};
|
||||
use common::model::parsed_packet::ParsedPacket;
|
||||
use common::model::rate_limit::*;
|
||||
use common::model::rate_limit::RateState;
|
||||
use network_types::ip::IpProto;
|
||||
|
||||
const CFG_PACKET_RATE: u32 = 0;
|
||||
const CFG_SYN_RATE: u32 = 1;
|
||||
const CFG_UDP_RATE: u32 = 2;
|
||||
const CFG_DNS_RATE: u32 = 3;
|
||||
const CFG_WINDOW_NS: u32 = 4;
|
||||
|
||||
#[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]
|
||||
@ -33,11 +29,11 @@ static IPV4_DNS_RATE_MAP: LruHashMap<IPv4, RateState> = LruHashMap::with_max_ent
|
||||
#[map]
|
||||
static IPV6_DNS_RATE_MAP: LruHashMap<IPv6, RateState> = LruHashMap::with_max_entries(MAX_TRACKED_IPS, 0);
|
||||
|
||||
pub fn should_drop(pkt: &ParsedPacket) -> bool {
|
||||
pub fn should_drop(pkt: &ParsedPacket) -> Option<u8> {
|
||||
match pkt.ip_version {
|
||||
4 => ipv4_should_drop(pkt),
|
||||
6 => ipv6_should_drop(pkt),
|
||||
_ => false,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,8 +48,6 @@ fn get_config(index: u32, default: u64) -> u64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the packet is a TCP SYN-only (no ACK) packet.
|
||||
/// For non-TCP packets (e.g. UDP), tcp_flags is 0, so this safely returns false.
|
||||
#[inline(always)]
|
||||
fn is_syn_only(pkt: &ParsedPacket) -> bool {
|
||||
matches!(pkt.protocol, IpProto::Tcp)
|
||||
@ -92,63 +86,63 @@ fn check_rate<K>(
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv4_should_drop(pkt: &ParsedPacket) -> bool {
|
||||
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 true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_PKT);
|
||||
}
|
||||
|
||||
if is_syn_only(pkt) {
|
||||
if check_rate(&IPV4_SYN_RATE_MAP, &src_ip, now, window, get_config(CFG_SYN_RATE, DEFAULT_SYN_RATE)) {
|
||||
return true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_SYN);
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(pkt.protocol, IpProto::Udp) {
|
||||
if check_rate(&IPV4_UDP_RATE_MAP, &src_ip, now, window, get_config(CFG_UDP_RATE, DEFAULT_UDP_RATE)) {
|
||||
return true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_UDP);
|
||||
}
|
||||
}
|
||||
|
||||
if pkt.dst_port == 53 {
|
||||
if matches!(pkt.protocol, IpProto::Udp) && pkt.dst_port == 53 {
|
||||
if check_rate(&IPV4_DNS_RATE_MAP, &src_ip, now, window, get_config(CFG_DNS_RATE, DEFAULT_DNS_RATE)) {
|
||||
return true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_DNS);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
None
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv6_should_drop(pkt: &ParsedPacket) -> bool {
|
||||
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 true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_PKT);
|
||||
}
|
||||
|
||||
if is_syn_only(pkt) {
|
||||
if check_rate(&IPV6_SYN_RATE_MAP, &src_ip, now, window, get_config(CFG_SYN_RATE, DEFAULT_SYN_RATE)) {
|
||||
return true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_SYN);
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(pkt.protocol, IpProto::Udp) {
|
||||
if check_rate(&IPV6_UDP_RATE_MAP, &src_ip, now, window, get_config(CFG_UDP_RATE, DEFAULT_UDP_RATE)) {
|
||||
return true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_UDP);
|
||||
}
|
||||
}
|
||||
|
||||
if pkt.dst_port == 53 {
|
||||
if matches!(pkt.protocol, IpProto::Udp) && pkt.dst_port == 53 {
|
||||
if check_rate(&IPV6_DNS_RATE_MAP, &src_ip, now, window, get_config(CFG_DNS_RATE, DEFAULT_DNS_RATE)) {
|
||||
return true;
|
||||
return Some(DROP_REASON_RATE_LIMIT_DNS);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
None
|
||||
}
|
||||
|
||||
@ -4,12 +4,15 @@ mod action;
|
||||
|
||||
use aya_ebpf::bindings::xdp_action;
|
||||
use aya_ebpf::macros::{map, xdp};
|
||||
use aya_ebpf::maps::{Array, PerCpuArray, ProgramArray, XskMap};
|
||||
use aya_ebpf::maps::{Array, PerCpuArray, ProgramArray, RingBuf, XskMap};
|
||||
use common::ebpf::symmetric_hash::symmetric_queue_id;
|
||||
use aya_ebpf::programs::XdpContext;
|
||||
#[allow(unused_imports)]
|
||||
use aya_log_ebpf::info;
|
||||
use common::ebpf::parsing;
|
||||
use common::define::pipeline::*;
|
||||
use common::define::drop_reason::*;
|
||||
use common::model::drop_event::DropEvent;
|
||||
use common::model::parsed_packet::ParsedPacket;
|
||||
|
||||
use crate::action::{access_control, rate_limit, protocol_filter};
|
||||
@ -22,6 +25,19 @@ static NEXT_STAGE: Array<u32> = Array::with_max_entries(MAX_STAGES, 0);
|
||||
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)]
|
||||
unsafe fn chain_next(ctx: &XdpContext, current_id: u32) {
|
||||
@ -35,12 +51,20 @@ unsafe fn chain_next(ctx: &XdpContext, current_id: u32) {
|
||||
}
|
||||
}
|
||||
|
||||
#[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)]
|
||||
unsafe fn emit_drop_event(pkt: &ParsedPacket, reason: u8) {
|
||||
if let Some(mut entry) = DROP_EVENTS.reserve::<DropEvent>(0) {
|
||||
let event = entry.as_mut_ptr();
|
||||
(*event).timestamp_ns = aya_ebpf::helpers::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 as u8;
|
||||
(*event).reason = reason;
|
||||
(*event).ip_version = pkt.ip_version;
|
||||
(*event)._pad = 0;
|
||||
entry.submit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +100,12 @@ unsafe fn try_access_control(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -85,7 +114,12 @@ unsafe fn try_access_control(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -114,7 +148,8 @@ unsafe fn try_rate_limit(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let pkt = &*ptr;
|
||||
if rate_limit::should_drop(pkt) {
|
||||
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);
|
||||
@ -125,7 +160,7 @@ unsafe fn try_rate_limit(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
#[xdp]
|
||||
pub fn protocol_filter(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
match try_service(&ctx) {
|
||||
match try_protocol_filter(&ctx) {
|
||||
Ok(action) => action,
|
||||
Err(_) => {
|
||||
chain_next(&ctx, STAGE_SERVICE);
|
||||
@ -136,20 +171,23 @@ pub fn protocol_filter(ctx: XdpContext) -> u32 {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn try_service(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
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 {
|
||||
4 => {
|
||||
if protocol_filter::ipv4_service_rule_violation(start, end, pkt) {
|
||||
emit_drop_event(pkt, DROP_REASON_PROTOCOL_FILTER);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
if protocol_filter::ipv6_service_rule_violation(start, end, pkt) {
|
||||
emit_drop_event(pkt, DROP_REASON_PROTOCOL_FILTER);
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
@ -160,9 +198,18 @@ unsafe fn try_service(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn compute_symmetric_queue_id() -> Option<u32> {
|
||||
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 { (*ctx.ctx).rx_queue_index };
|
||||
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,
|
||||
|
||||
@ -7,6 +7,6 @@ edition = "2024"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
|
||||
@ -7,37 +7,49 @@ edition = "2024"
|
||||
common = { path = "../common", features = ["user"] }
|
||||
macros = { path = "../macros" }
|
||||
|
||||
actix = "0.13.5"
|
||||
actix-cors = "0.7.1"
|
||||
actix-web = "4.11.0"
|
||||
actix-ws = "0.4.0"
|
||||
# eBPF userspace
|
||||
aya = { workspace = true }
|
||||
aya-log = { workspace = true }
|
||||
network-types = { workspace = true }
|
||||
crossbeam = "0.8.4"
|
||||
futures-util = "0.3.30"
|
||||
libc = { workspace = true }
|
||||
mime_guess = "2.0.5"
|
||||
parking_lot = "0.12.5"
|
||||
rust-embed = "8.7.2"
|
||||
serde = { workspace = true }
|
||||
serde_json = "1.0.143"
|
||||
sysinfo = "0.38.2"
|
||||
thiserror = "2.0.3"
|
||||
tokio = { version = "1.40.0", features = ["full", "macros"] }
|
||||
tokio-tungstenite = "0.28.0"
|
||||
toml = "1.0.3"
|
||||
tracing = "0.1.41"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||
url = "2.5.7"
|
||||
xsk-rs = { workspace = true }
|
||||
maxminddb = "0.27.1"
|
||||
lru = "0.16.2"
|
||||
futures = "0.3.31"
|
||||
tract-onnx = "0.22.0"
|
||||
#csv = "1.4.0"
|
||||
#anyhow = "1.0.100"
|
||||
libxdp-sys = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
# Web
|
||||
actix = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
actix-cors = { workspace = true }
|
||||
actix-ws = { workspace = true }
|
||||
rust-embed = "8.11.0"
|
||||
mime_guess = "2.0.5"
|
||||
url = "2.5.8"
|
||||
tokio-tungstenite = "0.28.0"
|
||||
|
||||
# Serialization
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = "1.0.7"
|
||||
|
||||
# Async
|
||||
tokio = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
crossbeam = { workspace = true }
|
||||
|
||||
# Logging
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
# ML
|
||||
tract-onnx = { workspace = true }
|
||||
|
||||
# Utilities
|
||||
parking_lot = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
maxminddb = { workspace = true }
|
||||
ipnetwork = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cargo_metadata = { workspace = true }
|
||||
|
||||
@ -8,16 +8,16 @@ use std::time::SystemTime;
|
||||
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind};
|
||||
|
||||
fn main() {
|
||||
build_ingress_ebpf();
|
||||
build_egress_ebpf();
|
||||
build_ebpf_package("ingress-ebpf", "ingress-ebpf");
|
||||
build_ebpf_package("egress-ebpf", "egress-ebpf");
|
||||
build_frontend();
|
||||
}
|
||||
|
||||
fn build_ingress_ebpf() {
|
||||
fn build_ebpf_package(package_name: &str, target_subdir: &str) {
|
||||
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
|
||||
let ebpf_package = packages
|
||||
.into_iter()
|
||||
.find(|Package { name, .. }| **name == "ingress-ebpf")
|
||||
.find(|Package { name, .. }| **name == *package_name)
|
||||
.unwrap();
|
||||
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
@ -42,6 +42,7 @@ fn build_ingress_ebpf() {
|
||||
let ebpf_dir = manifest_path.parent().unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed={}", ebpf_dir.as_str());
|
||||
println!("cargo:rerun-if-changed=../common/src");
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args([
|
||||
@ -62,127 +63,7 @@ fn build_ingress_ebpf() {
|
||||
}
|
||||
cmd.current_dir(ebpf_dir);
|
||||
|
||||
let ebpf_target_dir = out_dir.join("../ingress-ebpf");
|
||||
cmd.arg("--target-dir").arg(&ebpf_target_dir);
|
||||
|
||||
let mut child = cmd
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}"));
|
||||
let Child { stdout, stderr, .. } = &mut child;
|
||||
|
||||
let stderr = stderr.take().unwrap();
|
||||
let stderr = BufReader::new(stderr);
|
||||
let stderr = std::thread::spawn(move || {
|
||||
for line in stderr.lines() {
|
||||
let line = line.unwrap();
|
||||
println!("{line}");
|
||||
}
|
||||
});
|
||||
|
||||
let stdout = stdout.take().unwrap();
|
||||
let stdout = BufReader::new(stdout);
|
||||
let mut executables = Vec::new();
|
||||
for message in Message::parse_stream(stdout) {
|
||||
#[allow(clippy::collapsible_match)]
|
||||
match message.expect("valid JSON") {
|
||||
Message::CompilerArtifact(Artifact {
|
||||
executable,
|
||||
target: Target { name, .. },
|
||||
..
|
||||
}) => {
|
||||
if let Some(executable) = executable {
|
||||
executables.push((name, executable.into_std_path_buf()));
|
||||
}
|
||||
}
|
||||
Message::CompilerMessage(CompilerMessage { message, .. }) => {
|
||||
for line in message.rendered.unwrap_or_default().split('\n') {
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
Message::TextLine(line) => {
|
||||
println!("{line}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}"));
|
||||
assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}");
|
||||
|
||||
stderr.join().map_err(std::panic::resume_unwind).unwrap();
|
||||
|
||||
for (name, binary) in executables {
|
||||
let dst = out_dir.join(name);
|
||||
let _: u64 =
|
||||
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
|
||||
}
|
||||
} else {
|
||||
let Package { targets, .. } = ebpf_package;
|
||||
for Target { name, kind, .. } in targets {
|
||||
if *kind != [TargetKind::Bin] {
|
||||
continue;
|
||||
}
|
||||
let dst = out_dir.join(name);
|
||||
fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_egress_ebpf() {
|
||||
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
|
||||
let ebpf_package = packages
|
||||
.into_iter()
|
||||
.find(|Package { name, .. }| **name == "egress-ebpf")
|
||||
.unwrap();
|
||||
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
let out_dir = PathBuf::from(out_dir);
|
||||
|
||||
let endian = env::var_os("CARGO_CFG_TARGET_ENDIAN").unwrap();
|
||||
let target = if endian == "big" {
|
||||
"bpfeb"
|
||||
} else if endian == "little" {
|
||||
"bpfel"
|
||||
} else {
|
||||
panic!("unsupported endian={:?}", endian)
|
||||
};
|
||||
|
||||
let build_ebpf = true;
|
||||
if build_ebpf {
|
||||
let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||
|
||||
let target = format!("{target}-unknown-none");
|
||||
|
||||
let Package { manifest_path, .. } = ebpf_package;
|
||||
let ebpf_dir = manifest_path.parent().unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed={}", ebpf_dir.as_str());
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args([
|
||||
"build",
|
||||
"-Z",
|
||||
"build-std=core",
|
||||
"--bins",
|
||||
"--message-format=json",
|
||||
"--release",
|
||||
"--target",
|
||||
&target,
|
||||
]);
|
||||
|
||||
cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch);
|
||||
cmd.env("CARGO_TERM_COLOR", "always");
|
||||
|
||||
for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
|
||||
cmd.env_remove(key);
|
||||
}
|
||||
cmd.current_dir(ebpf_dir);
|
||||
|
||||
let ebpf_target_dir = out_dir.join("../egress-ebpf");
|
||||
let ebpf_target_dir = out_dir.join(format!("../{target_subdir}"));
|
||||
cmd.arg("--target-dir").arg(&ebpf_target_dir);
|
||||
|
||||
let mut child = cmd
|
||||
|
||||
@ -149,7 +149,6 @@ impl<T: NativeConvert + Pod> MapWrapper<T> {
|
||||
|
||||
fn add(&mut self, ip: T, port: Port) -> Result<(), Error> {
|
||||
if port == 0 {
|
||||
// port 0 in API = match all ports
|
||||
self.map
|
||||
.insert(ip, PortRule::new_match_all(), 0)
|
||||
.map_err(EbpfError::MapOperationError)?;
|
||||
@ -159,7 +158,7 @@ impl<T: NativeConvert + Pod> MapWrapper<T> {
|
||||
let mut rule = self.map.get(&ip, 0).unwrap_or_else(|_| PortRule::new_empty());
|
||||
|
||||
if rule.is_match_all() {
|
||||
return Ok(()); // already matching all
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !rule.add_port(port) {
|
||||
@ -174,7 +173,6 @@ impl<T: NativeConvert + Pod> MapWrapper<T> {
|
||||
|
||||
fn remove(&mut self, ip: T, port: Port) -> Result<(), Error> {
|
||||
if port == 0 {
|
||||
// port 0 in API = remove entire IP
|
||||
self.map.remove(&ip).map_err(EbpfError::MapOperationError)?;
|
||||
return Ok(());
|
||||
}
|
||||
@ -182,7 +180,6 @@ impl<T: NativeConvert + Pod> MapWrapper<T> {
|
||||
let mut rule = self.map.get(&ip, 0).map_err(|_| EbpfError::IpDoesNotExist)?;
|
||||
|
||||
if rule.is_match_all() {
|
||||
// Can't remove a single port from match_all — remove the whole IP
|
||||
self.map.remove(&ip).map_err(EbpfError::MapOperationError)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
247
net-guardia/src/core/ebpf/dns_filter.rs
Normal file
247
net-guardia/src/core/ebpf/dns_filter.rs
Normal file
@ -0,0 +1,247 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use common::model::dns_name::DnsName;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::Error;
|
||||
|
||||
pub struct DnsFilter {
|
||||
blacklist: RwLock<HashSet<DnsName>>,
|
||||
}
|
||||
|
||||
impl DnsFilter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
blacklist: RwLock::new(HashSet::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_domain(&self, domain: &str) -> Result<(), Error> {
|
||||
let name = domain_to_wire_format(domain)?;
|
||||
self.blacklist.write().insert(name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_domain(&self, domain: &str) -> Result<(), Error> {
|
||||
let name = domain_to_wire_format(domain)?;
|
||||
self.blacklist.write().remove(&name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_domains(&self) -> Vec<String> {
|
||||
self.blacklist
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|name| wire_format_to_domain(name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if a DNS query name (in wire format) or any of its parent domains is blacklisted.
|
||||
pub fn is_blacklisted(&self, name: &DnsName, name_len: usize) -> bool {
|
||||
let bl = self.blacklist.read();
|
||||
if bl.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// Check exact match
|
||||
if bl.contains(name) {
|
||||
return true;
|
||||
}
|
||||
// Check parent domains
|
||||
let mut offset: usize = 0;
|
||||
loop {
|
||||
if offset >= name_len || offset >= 128 {
|
||||
break;
|
||||
}
|
||||
let lbl = name.data[offset] as usize;
|
||||
if lbl == 0 {
|
||||
break;
|
||||
}
|
||||
offset += 1 + lbl;
|
||||
if offset >= name_len || offset >= 128 {
|
||||
break;
|
||||
}
|
||||
if name.data[offset] == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut parent = DnsName::zeroed();
|
||||
let remaining = name_len - offset;
|
||||
parent.data[..remaining.min(128)]
|
||||
.copy_from_slice(&name.data[offset..offset + remaining.min(128)]);
|
||||
if bl.contains(&parent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Parse DNS query name from raw packet bytes.
|
||||
/// Returns the DNS name in wire format and the name length, or None if not a DNS query.
|
||||
pub fn parse_query_name(raw: &[u8]) -> Option<(DnsName, usize)> {
|
||||
if raw.len() < 14 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let eth_type = u16::from_be_bytes([raw[12], raw[13]]);
|
||||
let l3_header_len = match eth_type {
|
||||
0x0800 => {
|
||||
// IPv4
|
||||
if raw.len() < 24 {
|
||||
return None;
|
||||
}
|
||||
let ihl = (raw[14] & 0x0F) as usize * 4;
|
||||
// Check protocol is UDP (17)
|
||||
if raw[14 + 9] != 17 {
|
||||
return None;
|
||||
}
|
||||
ihl
|
||||
}
|
||||
0x86DD => {
|
||||
// IPv6
|
||||
if raw.len() < 54 + 8 {
|
||||
return None;
|
||||
}
|
||||
// Check next header is UDP (17)
|
||||
if raw[14 + 6] != 17 {
|
||||
return None;
|
||||
}
|
||||
40
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let udp_start = 14 + l3_header_len;
|
||||
if udp_start + 8 > raw.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check destination port is 53
|
||||
let dst_port = u16::from_be_bytes([raw[udp_start + 2], raw[udp_start + 3]]);
|
||||
if dst_port != 53 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dns_header_offset = udp_start + 8;
|
||||
if dns_header_offset + 12 > raw.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// QR bit must be 0 (query, not response)
|
||||
if raw[dns_header_offset + 2] & 0x80 != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// QDCOUNT must be > 0
|
||||
if raw[dns_header_offset + 4] == 0 && raw[dns_header_offset + 5] == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dns_qname_offset = dns_header_offset + 12;
|
||||
if dns_qname_offset >= raw.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut name = DnsName::zeroed();
|
||||
let mut pos = dns_qname_offset;
|
||||
let mut out: usize = 0;
|
||||
for _ in 0..32 {
|
||||
if pos >= raw.len() {
|
||||
return None;
|
||||
}
|
||||
let ll = raw[pos] as usize;
|
||||
if ll == 0 {
|
||||
if out < 128 {
|
||||
name.data[out] = 0;
|
||||
}
|
||||
return Some((name, out + 1));
|
||||
}
|
||||
if ll >= 64 {
|
||||
return None;
|
||||
}
|
||||
if out + 1 + ll >= 128 {
|
||||
return None;
|
||||
}
|
||||
if pos + 1 + ll > raw.len() {
|
||||
return None;
|
||||
}
|
||||
name.data[out] = ll as u8;
|
||||
out += 1;
|
||||
for j in 0..ll {
|
||||
let mut b = raw[pos + 1 + j];
|
||||
if b >= b'A' && b <= b'Z' {
|
||||
b += 32;
|
||||
}
|
||||
name.data[out] = b;
|
||||
out += 1;
|
||||
}
|
||||
pos += 1 + ll;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a human-readable domain name (e.g., "example.com") to DNS wire format.
|
||||
/// The result is a DnsName with lowercase, length-prefixed labels, zero-terminated and zero-padded.
|
||||
fn domain_to_wire_format(domain: &str) -> Result<DnsName, Error> {
|
||||
let domain = domain.trim().trim_end_matches('.').to_lowercase();
|
||||
let mut name = DnsName::zeroed();
|
||||
let mut pos: usize = 0;
|
||||
|
||||
for label in domain.split('.') {
|
||||
let label_bytes = label.as_bytes();
|
||||
let label_len = label_bytes.len();
|
||||
if label_len == 0 || label_len >= 64 {
|
||||
return Err(MiscError::InvalidDnsName {
|
||||
reason: format!("invalid label length: {}", label_len),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
if pos + 1 + label_len >= 128 {
|
||||
return Err(MiscError::InvalidDnsName {
|
||||
reason: format!("domain name too long: {}", domain),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
name.data[pos] = label_len as u8;
|
||||
pos += 1;
|
||||
name.data[pos..pos + label_len].copy_from_slice(label_bytes);
|
||||
pos += label_len;
|
||||
}
|
||||
|
||||
// Terminating zero byte
|
||||
if pos < 128 {
|
||||
name.data[pos] = 0;
|
||||
}
|
||||
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
/// Convert DNS wire format back to a human-readable domain name.
|
||||
fn wire_format_to_domain(name: &DnsName) -> Option<String> {
|
||||
let mut labels: Vec<String> = Vec::new();
|
||||
let mut pos: usize = 0;
|
||||
|
||||
loop {
|
||||
if pos >= 128 {
|
||||
break;
|
||||
}
|
||||
let label_len = name.data[pos] as usize;
|
||||
if label_len == 0 {
|
||||
break;
|
||||
}
|
||||
if label_len >= 64 || pos + 1 + label_len > 128 {
|
||||
return None;
|
||||
}
|
||||
pos += 1;
|
||||
let label = core::str::from_utf8(&name.data[pos..pos + label_len]).ok()?;
|
||||
labels.push(label.to_string());
|
||||
pos += label_len;
|
||||
}
|
||||
|
||||
if labels.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(labels.join("."))
|
||||
}
|
||||
}
|
||||
167
net-guardia/src/core/ebpf/drop_monitor.rs
Normal file
167
net-guardia/src/core/ebpf/drop_monitor.rs
Normal file
@ -0,0 +1,167 @@
|
||||
use std::sync::Arc;
|
||||
use std::mem;
|
||||
use std::time::Duration;
|
||||
|
||||
use aya::maps::{MapData, RingBuf};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
|
||||
use common::define::drop_reason::*;
|
||||
use common::model::drop_event::DropEvent as RawDropEvent;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
const DROP_CHANNEL_CAPACITY: usize = 100;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DropEventMessage {
|
||||
pub timestamp_ns: u64,
|
||||
pub src_ip: String,
|
||||
pub dst_ip: String,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub protocol: u8,
|
||||
pub reason: String,
|
||||
pub ip_version: u8,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize)]
|
||||
pub struct DropCounters {
|
||||
pub acl_blacklist: u64,
|
||||
pub rate_limit_pkt: u64,
|
||||
pub rate_limit_syn: u64,
|
||||
pub rate_limit_udp: u64,
|
||||
pub rate_limit_dns: u64,
|
||||
pub protocol_filter: u64,
|
||||
pub dns_blacklist: u64,
|
||||
pub geo_block: u64,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
pub struct DropMonitor {
|
||||
broadcast_tx: broadcast::Sender<DropEventMessage>,
|
||||
counters: Mutex<DropCounters>,
|
||||
}
|
||||
|
||||
impl DropMonitor {
|
||||
pub fn new() -> Self {
|
||||
let (tx, _) = broadcast::channel(DROP_CHANNEL_CAPACITY);
|
||||
Self {
|
||||
broadcast_tx: tx,
|
||||
counters: Mutex::new(DropCounters::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<DropEventMessage> {
|
||||
self.broadcast_tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn get_counters(&self) -> DropCounters {
|
||||
self.counters.lock().clone()
|
||||
}
|
||||
|
||||
fn process_event(&self, raw: &RawDropEvent) {
|
||||
// Update counters
|
||||
{
|
||||
let mut c = self.counters.lock();
|
||||
c.total += 1;
|
||||
match raw.reason {
|
||||
DROP_REASON_ACL_BLACKLIST => c.acl_blacklist += 1,
|
||||
DROP_REASON_RATE_LIMIT_PKT => c.rate_limit_pkt += 1,
|
||||
DROP_REASON_RATE_LIMIT_SYN => c.rate_limit_syn += 1,
|
||||
DROP_REASON_RATE_LIMIT_UDP => c.rate_limit_udp += 1,
|
||||
DROP_REASON_RATE_LIMIT_DNS => c.rate_limit_dns += 1,
|
||||
DROP_REASON_PROTOCOL_FILTER => c.protocol_filter += 1,
|
||||
DROP_REASON_DNS_BLACKLIST => c.dns_blacklist += 1,
|
||||
DROP_REASON_GEO_BLOCK => c.geo_block += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let reason_str = reason_to_str(raw.reason);
|
||||
|
||||
// Format IPs based on version
|
||||
let (src_ip, dst_ip) = format_ips(raw);
|
||||
|
||||
let msg = DropEventMessage {
|
||||
timestamp_ns: raw.timestamp_ns,
|
||||
src_ip,
|
||||
dst_ip,
|
||||
src_port: raw.src_port,
|
||||
dst_port: raw.dst_port,
|
||||
protocol: raw.protocol,
|
||||
reason: reason_str.to_string(),
|
||||
ip_version: raw.ip_version,
|
||||
};
|
||||
|
||||
let _ = self.broadcast_tx.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DropMonitor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_ips(raw: &RawDropEvent) -> (String, String) {
|
||||
match raw.ip_version {
|
||||
4 => {
|
||||
let src = format!("{}.{}.{}.{}", raw.src_ip[0], raw.src_ip[1], raw.src_ip[2], raw.src_ip[3]);
|
||||
let dst = format!("{}.{}.{}.{}", raw.dst_ip[0], raw.dst_ip[1], raw.dst_ip[2], raw.dst_ip[3]);
|
||||
(src, dst)
|
||||
}
|
||||
_ => {
|
||||
// IPv6 - format as hex
|
||||
let src = format_ipv6(&raw.src_ip);
|
||||
let dst = format_ipv6(&raw.dst_ip);
|
||||
(src, dst)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_ipv6(bytes: &[u8; 16]) -> String {
|
||||
std::net::Ipv6Addr::from(*bytes).to_string()
|
||||
}
|
||||
|
||||
fn reason_to_str(reason: u8) -> &'static str {
|
||||
match reason {
|
||||
DROP_REASON_ACL_BLACKLIST => "acl_blacklist",
|
||||
DROP_REASON_RATE_LIMIT_PKT => "rate_limit_packet",
|
||||
DROP_REASON_RATE_LIMIT_SYN => "rate_limit_syn",
|
||||
DROP_REASON_RATE_LIMIT_UDP => "rate_limit_udp",
|
||||
DROP_REASON_RATE_LIMIT_DNS => "rate_limit_dns",
|
||||
DROP_REASON_PROTOCOL_FILTER => "protocol_filter",
|
||||
DROP_REASON_DNS_BLACKLIST => "dns_blacklist",
|
||||
DROP_REASON_GEO_BLOCK => "geo_block",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the ring buffer consumer as a tokio task. Returns a shutdown sender.
|
||||
pub async fn start_consumer(
|
||||
ring_buf: RingBuf<MapData>,
|
||||
monitor: Arc<DropMonitor>,
|
||||
) -> oneshot::Sender<()> {
|
||||
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut ring_buf = ring_buf;
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(100));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut shutdown_rx => break,
|
||||
_ = interval.tick() => {}
|
||||
}
|
||||
|
||||
while let Some(item) = ring_buf.next() {
|
||||
if item.len() >= mem::size_of::<RawDropEvent>() {
|
||||
let event = unsafe { &*(item.as_ptr() as *const RawDropEvent) };
|
||||
monitor.process_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
shutdown_tx
|
||||
}
|
||||
186
net-guardia/src/core/ebpf/geo_block.rs
Normal file
186
net-guardia/src/core/ebpf/geo_block.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use std::collections::{HashMap as StdHashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use aya::maps::lpm_trie::{Key, LpmTrie};
|
||||
use aya::maps::MapData;
|
||||
use aya::Ebpf;
|
||||
use ipnetwork::IpNetwork;
|
||||
use macros::log;
|
||||
use maxminddb::{geoip2, Reader};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::system::SystemLog;
|
||||
|
||||
/// Pre-indexed GeoIP prefix table, built once at startup.
|
||||
struct GeoIndex {
|
||||
v4: StdHashMap<String, Vec<(u32, u32)>>, // country -> [(ip_be, prefix_len)]
|
||||
v6: StdHashMap<String, Vec<(u128, u32)>>,
|
||||
}
|
||||
|
||||
pub struct GeoBlock {
|
||||
geo_block_v4: RwLock<LpmTrie<MapData, u32, u8>>,
|
||||
geo_block_v6: RwLock<LpmTrie<MapData, u128, u8>>,
|
||||
blocked_countries: RwLock<HashSet<String>>,
|
||||
index: Arc<GeoIndex>,
|
||||
}
|
||||
|
||||
impl GeoBlock {
|
||||
pub fn new(ebpf: &mut Ebpf, app_config: &AppConfig) -> Result<Self, Error> {
|
||||
let v4_map = ebpf.take_map("GEO_BLOCK_V4").ok_or(EbpfError::MapNotFound)?;
|
||||
let v4_trie = LpmTrie::try_from(v4_map).map_err(EbpfError::MapOperationError)?;
|
||||
|
||||
let v6_map = ebpf.take_map("GEO_BLOCK_V6").ok_or(EbpfError::MapNotFound)?;
|
||||
let v6_trie = LpmTrie::try_from(v6_map).map_err(EbpfError::MapOperationError)?;
|
||||
|
||||
let db_path = &app_config.misc.geoip_db_name;
|
||||
let reader = Reader::open_readfile(db_path)
|
||||
.map_err(|e| MiscError::GeoIPDatabaseError {
|
||||
path: db_path.clone(),
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let index = Self::build_index(&reader)?;
|
||||
|
||||
Ok(Self {
|
||||
geo_block_v4: RwLock::new(v4_trie),
|
||||
geo_block_v6: RwLock::new(v6_trie),
|
||||
blocked_countries: RwLock::new(HashSet::new()),
|
||||
index: Arc::new(index),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build index from MaxMind DB at startup. One-time cost.
|
||||
fn build_index(reader: &Reader<Vec<u8>>) -> Result<GeoIndex, Error> {
|
||||
let mut v4: StdHashMap<String, Vec<(u32, u32)>> = StdHashMap::new();
|
||||
let mut v6: StdHashMap<String, Vec<(u128, u32)>> = StdHashMap::new();
|
||||
|
||||
let ipv4_all: IpNetwork = "0.0.0.0/0".parse().unwrap();
|
||||
if let Ok(iter) = reader.within(ipv4_all, Default::default()) {
|
||||
for result in iter {
|
||||
let Ok(lookup) = result else { continue };
|
||||
let Ok(network) = lookup.network() else { continue };
|
||||
let Ok(Some(city)) = lookup.decode::<geoip2::City>() else { continue };
|
||||
let Some(code) = city.country.iso_code else { continue };
|
||||
let code = code.to_uppercase();
|
||||
|
||||
if let IpNetwork::V4(v4_net) = network {
|
||||
let ip_be = u32::from(v4_net.ip()).to_be();
|
||||
v4.entry(code).or_default().push((ip_be, v4_net.prefix() as u32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ipv6_all: IpNetwork = "::/0".parse().unwrap();
|
||||
if let Ok(iter) = reader.within(ipv6_all, Default::default()) {
|
||||
for result in iter {
|
||||
let Ok(lookup) = result else { continue };
|
||||
let Ok(network) = lookup.network() else { continue };
|
||||
let Ok(Some(city)) = lookup.decode::<geoip2::City>() else { continue };
|
||||
let Some(code) = city.country.iso_code else { continue };
|
||||
let code = code.to_uppercase();
|
||||
|
||||
if let IpNetwork::V6(v6_net) = network {
|
||||
let ip_be = u128::from(v6_net.ip()).to_be();
|
||||
v6.entry(code).or_default().push((ip_be, v6_net.prefix() as u32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GeoIndex { v4, v6 })
|
||||
}
|
||||
|
||||
/// Block multiple countries at once, rebuilding tries only once.
|
||||
pub fn block_countries(&self, country_codes: &[String]) -> Result<u64, Error> {
|
||||
{
|
||||
let mut countries = self.blocked_countries.write();
|
||||
for code in country_codes {
|
||||
let upper = code.trim().to_uppercase();
|
||||
if upper.len() == 2 && upper.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||
countries.insert(upper);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.rebuild_tries()
|
||||
}
|
||||
|
||||
/// Unblock multiple countries at once, rebuilding tries only once.
|
||||
pub fn unblock_countries(&self, country_codes: &[String]) -> Result<u64, Error> {
|
||||
{
|
||||
let mut countries = self.blocked_countries.write();
|
||||
for code in country_codes {
|
||||
countries.remove(&code.trim().to_uppercase());
|
||||
}
|
||||
}
|
||||
self.rebuild_tries()
|
||||
}
|
||||
|
||||
pub fn get_blocked_countries(&self) -> Vec<String> {
|
||||
self.blocked_countries.read().iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// Rebuild LPM tries from pre-indexed data. Fast — no DB scan.
|
||||
fn rebuild_tries(&self) -> Result<u64, Error> {
|
||||
let countries = self.blocked_countries.read().clone();
|
||||
|
||||
// Collect entries from index (no DB scan)
|
||||
let mut v4_entries: Vec<(Key<u32>, u8)> = Vec::new();
|
||||
let mut v6_entries: Vec<(Key<u128>, u8)> = Vec::new();
|
||||
|
||||
for code in &countries {
|
||||
if let Some(prefixes) = self.index.v4.get(code) {
|
||||
for &(ip_be, prefix_len) in prefixes {
|
||||
v4_entries.push((Key::new(prefix_len, ip_be), 1u8));
|
||||
}
|
||||
}
|
||||
if let Some(prefixes) = self.index.v6.get(code) {
|
||||
for &(ip_be, prefix_len) in prefixes {
|
||||
v6_entries.push((Key::new(prefix_len, ip_be), 1u8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock, clear, insert
|
||||
let mut v4_trie = self.geo_block_v4.write();
|
||||
let mut v6_trie = self.geo_block_v6.write();
|
||||
Self::clear_trie_v4(&mut v4_trie);
|
||||
Self::clear_trie_v6(&mut v6_trie);
|
||||
|
||||
let mut count = 0u64;
|
||||
for (key, val) in &v4_entries {
|
||||
if v4_trie.insert(key, *val, 0).is_ok() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
for (key, val) in &v6_entries {
|
||||
if v6_trie.insert(key, *val, 0).is_ok() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn clear_trie_v4(trie: &mut LpmTrie<MapData, u32, u8>) {
|
||||
let keys: Vec<Key<u32>> = trie.iter()
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|(k, _)| k)
|
||||
.collect();
|
||||
for key in keys {
|
||||
let _ = trie.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_trie_v6(trie: &mut LpmTrie<MapData, u128, u8>) {
|
||||
let keys: Vec<Key<u128>> = trie.iter()
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|(k, _)| k)
|
||||
.collect();
|
||||
for key in keys {
|
||||
let _ = trie.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
pub mod access_control;
|
||||
pub mod dns_filter;
|
||||
pub mod drop_monitor;
|
||||
pub mod geo_block;
|
||||
pub mod rate_limit;
|
||||
pub mod protocol_filter;
|
||||
pub mod xsk_manager;
|
||||
@ -6,16 +9,22 @@ pub mod xsk_manager;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aya::Ebpf;
|
||||
use aya::maps::{MapData, RingBuf};
|
||||
use crossbeam::queue::SegQueue;
|
||||
use macros::log;
|
||||
use parking_lot::Mutex;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::core::ebpf::access_control::AccessControl;
|
||||
use crate::core::ebpf::dns_filter::DnsFilter;
|
||||
use crate::core::ebpf::drop_monitor::DropMonitor;
|
||||
use crate::core::ebpf::geo_block::GeoBlock;
|
||||
use crate::core::ebpf::rate_limit::RateLimitConfig;
|
||||
use crate::core::ebpf::protocol_filter::ProtocolFilter;
|
||||
use crate::core::ebpf::xsk_manager::XskManager;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::ml::engine::Engine;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
|
||||
@ -23,7 +32,11 @@ pub struct EbpfServices {
|
||||
pub xsk_manager: Arc<XskManager>,
|
||||
pub access_control: Arc<AccessControl>,
|
||||
pub protocol_filter: Arc<ProtocolFilter>,
|
||||
pub dns_filter: Arc<DnsFilter>,
|
||||
pub geo_block: Arc<GeoBlock>,
|
||||
pub rate_limit: Arc<RateLimitConfig>,
|
||||
pub drop_monitor: Arc<DropMonitor>,
|
||||
drop_ring_buf: Mutex<Option<RingBuf<MapData>>>,
|
||||
pub shutdowns: SegQueue<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
@ -32,19 +45,36 @@ impl EbpfServices {
|
||||
let xsk_manager = XskManager::new(app_config.clone(), ingress_ebpf, egress_ebpf)?;
|
||||
let access_control = AccessControl::new(ingress_ebpf)?;
|
||||
let protocol_filter = ProtocolFilter::new(ingress_ebpf)?;
|
||||
let dns_filter = DnsFilter::new();
|
||||
let geo_block = GeoBlock::new(ingress_ebpf, &app_config)?;
|
||||
let rate_limit = RateLimitConfig::new(ingress_ebpf)?;
|
||||
let drop_monitor = Arc::new(DropMonitor::new());
|
||||
let drop_ring_buf = {
|
||||
let map = ingress_ebpf.take_map("DROP_EVENTS").ok_or(EbpfError::MapNotFound)?;
|
||||
RingBuf::try_from(map).map_err(EbpfError::MapOperationError)?
|
||||
};
|
||||
Ok(Self {
|
||||
xsk_manager: Arc::new(xsk_manager),
|
||||
access_control: Arc::new(access_control),
|
||||
protocol_filter: Arc::new(protocol_filter),
|
||||
dns_filter: Arc::new(dns_filter),
|
||||
geo_block: Arc::new(geo_block),
|
||||
rate_limit: Arc::new(rate_limit),
|
||||
drop_monitor,
|
||||
drop_ring_buf: Mutex::new(Some(drop_ring_buf)),
|
||||
shutdowns: SegQueue::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>, ml_engine: Arc<Engine>) -> Result<(), Error> {
|
||||
let xsk_manager = self.xsk_manager.clone();
|
||||
xsk_manager.run(Some(ml_engine), &self.shutdowns)?;
|
||||
xsk_manager.run(Some(ml_engine), Some(self.dns_filter.clone()), &self.shutdowns)?;
|
||||
|
||||
if let Some(ring_buf) = self.drop_ring_buf.lock().take() {
|
||||
let shutdown = drop_monitor::start_consumer(ring_buf, self.drop_monitor.clone()).await;
|
||||
self.shutdowns.push(shutdown);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -40,4 +40,24 @@ impl RateLimitConfig {
|
||||
self.config_map.lock().set(4, ns, 0).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_packet_rate(&self) -> Result<u64, Error> {
|
||||
self.config_map.lock().get(&0, 0).map_err(|e| EbpfError::MapOperationError(e).into())
|
||||
}
|
||||
|
||||
pub fn get_syn_rate(&self) -> Result<u64, Error> {
|
||||
self.config_map.lock().get(&1, 0).map_err(|e| EbpfError::MapOperationError(e).into())
|
||||
}
|
||||
|
||||
pub fn get_udp_rate(&self) -> Result<u64, Error> {
|
||||
self.config_map.lock().get(&2, 0).map_err(|e| EbpfError::MapOperationError(e).into())
|
||||
}
|
||||
|
||||
pub fn get_dns_rate(&self) -> Result<u64, Error> {
|
||||
self.config_map.lock().get(&3, 0).map_err(|e| EbpfError::MapOperationError(e).into())
|
||||
}
|
||||
|
||||
pub fn get_window_ns(&self) -> Result<u64, Error> {
|
||||
self.config_map.lock().get(&4, 0).map_err(|e| EbpfError::MapOperationError(e).into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ use tokio::sync::oneshot;
|
||||
use xsk_rs::config::{BindFlags, FrameSize, Interface, LibxdpFlags, QueueSize, SocketConfig, UmemConfig};
|
||||
use xsk_rs::{CompQueue, FillQueue, FrameDesc, RxQueue, Socket, TxQueue, Umem};
|
||||
|
||||
use crate::core::ebpf::dns_filter::DnsFilter;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::ml::engine::Engine;
|
||||
use crate::core::ml::flow_tracker::FlowTracker;
|
||||
@ -25,6 +26,36 @@ use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::ebpf::EbpfLog;
|
||||
use crate::utils::packet_parser::parse_packet;
|
||||
|
||||
/// Pre-allocated buffer pool to avoid per-packet malloc.
|
||||
struct BufferPool {
|
||||
buffers: Vec<Vec<u8>>,
|
||||
buffer_size: usize,
|
||||
max_capacity: usize,
|
||||
}
|
||||
|
||||
impl BufferPool {
|
||||
fn new(capacity: usize, buffer_size: usize) -> Self {
|
||||
let buffers = (0..capacity)
|
||||
.map(|_| Vec::with_capacity(buffer_size))
|
||||
.collect();
|
||||
Self { buffers, buffer_size, max_capacity: capacity * 2 }
|
||||
}
|
||||
|
||||
fn get(&mut self) -> Vec<u8> {
|
||||
self.buffers
|
||||
.pop()
|
||||
.unwrap_or_else(|| Vec::with_capacity(self.buffer_size))
|
||||
}
|
||||
|
||||
fn put(&mut self, mut buf: Vec<u8>) {
|
||||
buf.clear();
|
||||
if self.buffers.len() < self.max_capacity {
|
||||
self.buffers.push(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct XskManager {
|
||||
app_config: Arc<AppConfig>,
|
||||
@ -49,7 +80,7 @@ impl XskManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&self, ml_engine: Option<Arc<Engine>>, shutdowns: &SegQueue<oneshot::Sender<()>>) -> Result<(), Error> {
|
||||
pub fn run(&self, ml_engine: Option<Arc<Engine>>, dns_filter: Option<Arc<DnsFilter>>, shutdowns: &SegQueue<oneshot::Sender<()>>) -> Result<(), Error> {
|
||||
let network = self.app_config.network.clone();
|
||||
let combined_queue_count = network.combined_queue_count;
|
||||
|
||||
@ -66,6 +97,7 @@ impl XskManager {
|
||||
&network.egress_ifname,
|
||||
Direction::Ingress,
|
||||
tracker.clone(),
|
||||
dns_filter.clone(),
|
||||
)?;
|
||||
|
||||
let egress_xsk = XskPair::new(
|
||||
@ -75,6 +107,7 @@ impl XskManager {
|
||||
&network.ingress_ifname,
|
||||
Direction::Egress,
|
||||
tracker,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mut xsk_map = self.xsk_map.lock();
|
||||
@ -113,8 +146,11 @@ pub struct XskPair {
|
||||
comp_queue: CompQueue,
|
||||
tx: TxQueue,
|
||||
rx: RxQueue,
|
||||
frame_pool: Arc<Mutex<Vec<FrameDesc>>>,
|
||||
frame_pool: Vec<FrameDesc>,
|
||||
tracker: Option<Arc<Mutex<FlowTracker>>>,
|
||||
dns_filter: Option<Arc<DnsFilter>>,
|
||||
packet_buffer_size: usize,
|
||||
buffer_pool_capacity: usize,
|
||||
}
|
||||
|
||||
impl XskPair {
|
||||
@ -125,8 +161,9 @@ impl XskPair {
|
||||
_tx_ifname: &str,
|
||||
direction: Direction,
|
||||
tracker: Option<Arc<Mutex<FlowTracker>>>,
|
||||
dns_filter: Option<Arc<DnsFilter>>,
|
||||
) -> Result<Self, Error> {
|
||||
let rx_ifname_c = CString::new(rx_ifname).map_err(|_| SystemError::UnknownError)?;
|
||||
let rx_ifname_c = CString::new(rx_ifname).map_err(|_| SystemError::InvalidConfig)?;
|
||||
|
||||
let fill_queue_size = QueueSize::new(config.fill_queue_size).map_err(|_| SystemError::InvalidConfig)?;
|
||||
let comp_queue_size = QueueSize::new(config.comp_queue_size).map_err(|_| SystemError::InvalidConfig)?;
|
||||
@ -154,7 +191,6 @@ impl XskPair {
|
||||
|
||||
let interface = Interface::new(rx_ifname_c);
|
||||
|
||||
// SAFETY: Interface and umem are valid and outlive the socket
|
||||
let (tx, rx, queue) =
|
||||
unsafe { Socket::new(socket_config, &umem, &interface, queue_id).map_err(EbpfError::SocketSetFailed)? };
|
||||
|
||||
@ -165,7 +201,6 @@ impl XskPair {
|
||||
|
||||
let fill_frames: Vec<FrameDesc> = frame_descs.iter().take(fill_frames_count).copied().collect();
|
||||
|
||||
// SAFETY: Frame descriptors are valid and owned by this UMEM
|
||||
let submitted = unsafe { fill_queue.produce(&fill_frames) };
|
||||
if submitted != fill_frames.len() {
|
||||
return Err(EbpfError::FillQueueInitFailed.into());
|
||||
@ -180,8 +215,11 @@ impl XskPair {
|
||||
comp_queue,
|
||||
tx,
|
||||
rx,
|
||||
frame_pool: Arc::new(Mutex::new(pool_frames)),
|
||||
frame_pool: pool_frames,
|
||||
tracker,
|
||||
dns_filter,
|
||||
packet_buffer_size: config.packet_buffer_size,
|
||||
buffer_pool_capacity: config.buffer_pool_capacity,
|
||||
};
|
||||
|
||||
Ok(xsk_pair)
|
||||
@ -201,11 +239,11 @@ impl XskPair {
|
||||
.spawn(move || {
|
||||
let mut shutdown_rx = Some(shutdown_rx);
|
||||
let mut idle_count: u32 = 0;
|
||||
let mut buffer_pool = BufferPool::new(self.buffer_pool_capacity, self.packet_buffer_size);
|
||||
let mut comp_descs = vec![FrameDesc::default(); 256];
|
||||
let mut rx_descs = vec![FrameDesc::default(); 64];
|
||||
|
||||
loop {
|
||||
// Non-blocking shutdown check: try_recv avoids blocking the hot loop.
|
||||
// The idle backoff below (sleep_us) ensures we don't busy-spin when idle,
|
||||
// which also bounds how quickly we detect shutdown to at most 100us.
|
||||
if let Some(ref mut rx) = shutdown_rx {
|
||||
match rx.try_recv() {
|
||||
Ok(_) | Err(oneshot::error::TryRecvError::Closed) => {
|
||||
@ -217,17 +255,17 @@ impl XskPair {
|
||||
|
||||
let mut total_activity = 0;
|
||||
|
||||
match self.process_comp_queue() {
|
||||
match self.process_comp_queue(&mut comp_descs) {
|
||||
Ok(count) => total_activity += count,
|
||||
Err(e) => log!(EbpfLog::CompQueueError(format!("{:?}", e))),
|
||||
}
|
||||
|
||||
match self.process_rx_queue(&forward_tx) {
|
||||
match self.process_rx_queue(&forward_tx, &mut buffer_pool, &mut rx_descs) {
|
||||
Ok(count) => total_activity += count,
|
||||
Err(e) => log!(EbpfLog::RXQueueError(format!("{:?}", e))),
|
||||
}
|
||||
|
||||
match self.process_tx_queue(&forward_rx) {
|
||||
match self.process_tx_queue(&forward_rx, &mut buffer_pool, &mut comp_descs) {
|
||||
Ok(count) => total_activity += count,
|
||||
Err(e) => log!(EbpfLog::TXQueueError(format!("{:?}", e))),
|
||||
}
|
||||
@ -256,51 +294,65 @@ impl XskPair {
|
||||
})
|
||||
}
|
||||
|
||||
fn process_comp_queue(&mut self) -> Result<usize, EbpfError> {
|
||||
let mut comp_descs = vec![FrameDesc::default(); 256];
|
||||
|
||||
let nb_completed = unsafe { self.comp_queue.consume(&mut comp_descs) };
|
||||
fn process_comp_queue(&mut self, comp_descs: &mut [FrameDesc]) -> Result<usize, EbpfError> {
|
||||
let nb_completed = unsafe { self.comp_queue.consume(comp_descs) };
|
||||
|
||||
if nb_completed > 0 {
|
||||
let mut pool = self.frame_pool.lock();
|
||||
|
||||
for desc in comp_descs.iter().take(nb_completed) {
|
||||
pool.push(*desc);
|
||||
self.frame_pool.push(*desc);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nb_completed)
|
||||
}
|
||||
|
||||
fn process_rx_queue(&mut self, forward_tx: &Sender<Vec<u8>>) -> Result<usize, EbpfError> {
|
||||
let mut rx_descs = vec![FrameDesc::default(); 64];
|
||||
// SAFETY: rx_descs buffer is large enough for consume
|
||||
let rx_count = unsafe { self.rx.consume(&mut rx_descs) };
|
||||
fn process_rx_queue(&mut self, forward_tx: &Sender<Vec<u8>>, buffer_pool: &mut BufferPool, rx_descs: &mut [FrameDesc]) -> Result<usize, EbpfError> {
|
||||
let rx_count = unsafe { self.rx.consume(rx_descs) };
|
||||
|
||||
if rx_count > 0 {
|
||||
let is_ingress = self.direction == Direction::Ingress;
|
||||
|
||||
for rx_desc in rx_descs.iter().take(rx_count) {
|
||||
let lengths = rx_desc.lengths();
|
||||
let packet_len = lengths.data() as usize;
|
||||
|
||||
// SAFETY: rx_desc is valid and belongs to this UMEM
|
||||
let data = unsafe { self.umem.data(rx_desc) };
|
||||
let contents = data.contents();
|
||||
if packet_len > contents.len() {
|
||||
log!(EbpfLog::InvalidPacketLength);
|
||||
continue;
|
||||
}
|
||||
let packet_data = contents[..packet_len].to_vec();
|
||||
|
||||
if let Some(ref tracker) = self.tracker {
|
||||
Engine::process_packet(tracker, &packet_data, self.direction == Direction::Ingress);
|
||||
let raw = &contents[..packet_len];
|
||||
|
||||
// DNS blacklist check — drop blacklisted DNS queries before forwarding
|
||||
if let Some(ref dns) = self.dns_filter {
|
||||
if let Some((dns_name, name_len)) = DnsFilter::parse_query_name(raw) {
|
||||
if dns.is_blacklisted(&dns_name, name_len) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = forward_tx.try_send(packet_data) {
|
||||
// Parse directly from UMEM (zero-copy for ML path).
|
||||
// Only clone for the forwarding path afterwards.
|
||||
if let Some(ref tracker) = self.tracker {
|
||||
if let Some((packet_info, _)) = parse_packet(raw) {
|
||||
tracker.lock().process_packet(packet_info, is_ingress);
|
||||
}
|
||||
}
|
||||
|
||||
// Clone into pooled buffer for forwarding
|
||||
let mut buf = buffer_pool.get();
|
||||
buf.extend_from_slice(raw);
|
||||
if let Err(e) = forward_tx.try_send(buf) {
|
||||
match e {
|
||||
crossbeam::channel::TrySendError::Full(_) => {
|
||||
crossbeam::channel::TrySendError::Full(returned) => {
|
||||
buffer_pool.put(returned);
|
||||
log!(EbpfLog::ForwardChannelFull);
|
||||
}
|
||||
crossbeam::channel::TrySendError::Disconnected(_) => {
|
||||
crossbeam::channel::TrySendError::Disconnected(returned) => {
|
||||
buffer_pool.put(returned);
|
||||
log!(EbpfLog::ForwardChannelDisconnected);
|
||||
}
|
||||
}
|
||||
@ -318,7 +370,7 @@ impl XskPair {
|
||||
Ok(rx_count)
|
||||
}
|
||||
|
||||
fn process_tx_queue(&mut self, forward_rx: &Receiver<Vec<u8>>) -> Result<usize, EbpfError> {
|
||||
fn process_tx_queue(&mut self, forward_rx: &Receiver<Vec<u8>>, buffer_pool: &mut BufferPool, comp_descs: &mut [FrameDesc]) -> Result<usize, EbpfError> {
|
||||
let mut packets_to_send = Vec::with_capacity(64);
|
||||
while let Ok(packet) = forward_rx.try_recv() {
|
||||
packets_to_send.push(packet);
|
||||
@ -331,37 +383,32 @@ impl XskPair {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if let Err(e) = self.process_comp_queue() {
|
||||
if let Err(e) = self.process_comp_queue(comp_descs) {
|
||||
log!(EbpfLog::CompQueueError(format!("{:?}", e)));
|
||||
}
|
||||
|
||||
let pool_size = {
|
||||
let pool = self.frame_pool.lock();
|
||||
pool.len()
|
||||
};
|
||||
let total_packets = packets_to_send.len();
|
||||
|
||||
if pool_size == 0 {
|
||||
log!(EbpfLog::FramePoolExhausted(packets_to_send.len()));
|
||||
if self.frame_pool.is_empty() {
|
||||
for pkt in packets_to_send {
|
||||
buffer_pool.put(pkt);
|
||||
}
|
||||
log!(EbpfLog::FramePoolExhausted(total_packets));
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut frames = Vec::with_capacity(packets_to_send.len());
|
||||
{
|
||||
let mut pool = self.frame_pool.lock();
|
||||
let available = pool.len().min(packets_to_send.len());
|
||||
|
||||
for _ in 0..available {
|
||||
if let Some(frame) = pool.pop() {
|
||||
frames.push(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
let available = self.frame_pool.len().min(total_packets);
|
||||
let mut frames: Vec<FrameDesc> = self.frame_pool.drain(self.frame_pool.len() - available..).collect();
|
||||
|
||||
if frames.is_empty() {
|
||||
for pkt in packets_to_send {
|
||||
buffer_pool.put(pkt);
|
||||
}
|
||||
log!(EbpfLog::NoFramesAvailable);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let sent_count = frames.len();
|
||||
for (frame, packet) in frames.iter_mut().zip(packets_to_send.iter()) {
|
||||
unsafe {
|
||||
self.umem
|
||||
@ -372,15 +419,32 @@ impl XskPair {
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: Frames contain valid packet data written above
|
||||
let nb_submitted = unsafe { self.tx.produce(&frames) };
|
||||
|
||||
// Return unsubmitted frames to pool to prevent frame leak
|
||||
if nb_submitted < frames.len() {
|
||||
for frame in frames[nb_submitted..].iter() {
|
||||
self.frame_pool.push(*frame);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = self.tx.wakeup() {
|
||||
if e.kind() != std::io::ErrorKind::WouldBlock {
|
||||
log!(EbpfLog::TXWakeupFailed(e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// Log dropped packets when frames < packets
|
||||
let dropped = total_packets - sent_count;
|
||||
if dropped > 0 {
|
||||
log!(EbpfLog::FramePoolExhausted(dropped));
|
||||
}
|
||||
|
||||
// Return all buffers to pool
|
||||
for pkt in packets_to_send {
|
||||
buffer_pool.put(pkt);
|
||||
}
|
||||
|
||||
Ok(nb_submitted)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ pub struct AppConfig {
|
||||
pub http: HttpConfig,
|
||||
pub network: NetworkConfig,
|
||||
pub inference: InfConfig,
|
||||
#[allow(dead_code)]
|
||||
pub misc: MiscConfig,
|
||||
pub pipeline: PipelineConfig,
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ use tokio::task;
|
||||
|
||||
use crate::utils::ip_address;
|
||||
|
||||
// TODO: Wire into statistics endpoint when GeoIP enrichment is enabled
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct GeoLocation {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// net-guardia/src/core/ebpf/health.rs
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@ -29,10 +28,8 @@ pub struct SystemHealth {
|
||||
broadcast_tx: broadcast::Sender<SystemHealthMetrics>,
|
||||
ingress_interface: String,
|
||||
egress_interface: String,
|
||||
// management_interface: String,
|
||||
}
|
||||
|
||||
|
||||
impl SystemHealth {
|
||||
pub fn new(config: Arc<AppConfig>) -> Result<Self, Error> {
|
||||
let (broadcast_tx, _) = broadcast::channel(100);
|
||||
@ -44,7 +41,6 @@ impl SystemHealth {
|
||||
broadcast_tx,
|
||||
ingress_interface: config.network.ingress_ifname.clone(),
|
||||
egress_interface: config.network.egress_ifname.clone(),
|
||||
// management_interface: config.management_ifindex.clone(),
|
||||
};
|
||||
|
||||
Ok(health)
|
||||
@ -88,7 +84,6 @@ impl SystemHealth {
|
||||
&components,
|
||||
&self.ingress_interface,
|
||||
&self.egress_interface,
|
||||
// &self.management_interface,
|
||||
);
|
||||
|
||||
drop(system);
|
||||
@ -108,7 +103,6 @@ impl SystemHealth {
|
||||
components: &Components,
|
||||
ingress_interface: &str,
|
||||
egress_interface: &str,
|
||||
// management_interface: &str,
|
||||
) -> SystemHealthMetrics {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@ -134,7 +128,6 @@ impl SystemHealth {
|
||||
networks,
|
||||
ingress_interface,
|
||||
egress_interface,
|
||||
// management_interface,
|
||||
);
|
||||
|
||||
let load_average = System::load_average();
|
||||
@ -218,7 +211,6 @@ impl SystemHealth {
|
||||
networks: &Networks,
|
||||
ingress_interface: &str,
|
||||
egress_interface: &str,
|
||||
// management_interface: &str,
|
||||
) -> ConfiguredNetworkStats {
|
||||
let create_network_stats = |interface_name: &str| -> Option<NetworkStats> {
|
||||
networks.get(interface_name).map(|network| NetworkStats {
|
||||
@ -234,7 +226,6 @@ impl SystemHealth {
|
||||
|
||||
let ingress = create_network_stats(ingress_interface);
|
||||
let egress = create_network_stats(egress_interface);
|
||||
// let management = create_network_stats(management_interface);
|
||||
|
||||
if ingress.is_none() {
|
||||
log!(Health::InterfaceNotFound("Ingress".to_string(), ingress_interface.to_string()));
|
||||
@ -242,20 +233,13 @@ impl SystemHealth {
|
||||
if egress.is_none() {
|
||||
log!(Health::InterfaceNotFound("Egress".to_string(), egress_interface.to_string()));
|
||||
}
|
||||
// if management.is_none() {
|
||||
// warn!("Management interface '{}' not found", management_interface);
|
||||
// }
|
||||
|
||||
ConfiguredNetworkStats {
|
||||
ingress,
|
||||
egress,
|
||||
// management,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_current_metrics(&self) -> SystemHealthMetrics {
|
||||
// Read last cached metrics from background task, don't refresh here
|
||||
// to avoid racing with the background refresh_and_broadcast task
|
||||
let system = self.system.read().await;
|
||||
let networks = self.networks.read().await;
|
||||
let components = self.components.read().await;
|
||||
@ -266,7 +250,6 @@ impl SystemHealth {
|
||||
&components,
|
||||
&self.ingress_interface,
|
||||
&self.egress_interface,
|
||||
// &self.management_interface,
|
||||
)
|
||||
}
|
||||
|
||||
@ -328,11 +311,6 @@ impl SystemHealth {
|
||||
status.overall_healthy = false;
|
||||
status.issues.push("Egress interface not available".to_string());
|
||||
}
|
||||
// if metrics.network_stats.management.is_none() {
|
||||
// status
|
||||
// .warnings
|
||||
// .push("Management interface not available".to_string());
|
||||
// }
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
use std::time;
|
||||
|
||||
use crate::core::ml::engine::Engine;
|
||||
use crate::model::direction::Direction;
|
||||
use crate::model::flow_stats::{FlowStatsEntry, FlowSubscription, StatsSummary};
|
||||
|
||||
pub struct FlowStatistics {
|
||||
@ -31,23 +30,19 @@ impl FlowStatistics {
|
||||
|
||||
let mut flows = self.get_all_flows();
|
||||
|
||||
// Filter by direction
|
||||
if let Some(dir) = &sub.direction {
|
||||
flows.retain(|f| &f.direction == dir);
|
||||
}
|
||||
|
||||
// Filter by time window
|
||||
if let Some(window_secs) = sub.window_secs {
|
||||
let cutoff = now_us.saturating_sub(window_secs * 1_000_000);
|
||||
flows.retain(|f| f.last_seen_us >= cutoff);
|
||||
}
|
||||
|
||||
// Sort by total bytes descending
|
||||
flows.sort_by(|a, b| {
|
||||
(b.fwd_bytes + b.bwd_bytes).cmp(&(a.fwd_bytes + a.bwd_bytes))
|
||||
});
|
||||
|
||||
// Limit
|
||||
if let Some(n) = sub.top_n {
|
||||
flows.truncate(n.min(10000));
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use macros::log;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::error;
|
||||
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::ml_detection::DetectionResult;
|
||||
|
||||
const ALERT_CHANNEL_CAPACITY: usize = 100;
|
||||
@ -65,15 +66,14 @@ impl MLAlert {
|
||||
if self.broadcast_tx.receiver_count() > 0 {
|
||||
let alert = AlertMessage::from_detection_result(result);
|
||||
if let Err(e) = self.broadcast_tx.send(alert) {
|
||||
error!("Failed to broadcast ML alert: {}", e);
|
||||
log!(MLLog::BroadcastAlertFailed(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Default for MLAlert {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,17 +23,17 @@ impl InferenceConfig {
|
||||
pub fn load_file(file: &str) -> Result<Self, MLError> {
|
||||
let path = PathBuf::from("models").join(file);
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|_| MLError::ConfigLoadFailed { path: path.to_path_buf() })?;
|
||||
.map_err(|_| MLError::ConfigLoadFailed(path.to_path_buf()))?;
|
||||
let config: InferenceConfig = serde_json::from_str(&content)
|
||||
.map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
|
||||
.map_err(|e| MLError::ConfigParseFailed(e.to_string()))?;
|
||||
if config.ae_feature_names.is_empty() {
|
||||
return Err(MLError::ConfigParseFailed { reason: "ae_feature_names is empty".into() });
|
||||
return Err(MLError::ConfigParseFailed("ae_feature_names is empty"));
|
||||
}
|
||||
if config.ae_scaler_mean.len() != config.ae_feature_names.len() {
|
||||
return Err(MLError::ConfigParseFailed { reason: "scaler mean length mismatch".into() });
|
||||
return Err(MLError::ConfigParseFailed("scaler mean length mismatch"));
|
||||
}
|
||||
if config.ae_scaler_std.len() != config.ae_feature_names.len() {
|
||||
return Err(MLError::ConfigParseFailed { reason: "scaler std length mismatch".into() });
|
||||
return Err(MLError::ConfigParseFailed("scaler std length mismatch"));
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
@ -50,8 +50,4 @@ impl InferenceConfig {
|
||||
self.attack_labels.len()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_attack_label(&self, id: usize) -> Option<&String> {
|
||||
self.attack_labels.get(&id.to_string())
|
||||
}
|
||||
}
|
||||
@ -17,10 +17,9 @@ use super::traffic_logger::TrafficLogger;
|
||||
use super::alert::MLAlert;
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::ml_detection::{EngineConfig, InferenceStats};
|
||||
use crate::utils::packet_parser::parse_packet;
|
||||
|
||||
/// Per-thread tracker. Mutex is only contested during inference tick (every N seconds).
|
||||
/// Hot path (process_packet): lock is uncontended → ~15ns.
|
||||
/// Per-queue tracker. With symmetric hash in eBPF, both directions of a flow
|
||||
/// land on the same queue, so per-queue trackers correctly see bidirectional flows.
|
||||
pub type ThreadTracker = Arc<Mutex<FlowTracker>>;
|
||||
|
||||
pub struct Engine {
|
||||
@ -28,7 +27,6 @@ pub struct Engine {
|
||||
inference_pipeline: Arc<Inference>,
|
||||
aggregator: Mutex<AttackAggregator>,
|
||||
ml_alert: Arc<MLAlert>,
|
||||
max_flows_per_thread: usize,
|
||||
min_packets: usize,
|
||||
batch_size: usize,
|
||||
inference_interval_secs: u64,
|
||||
@ -50,8 +48,7 @@ impl Engine {
|
||||
let min_detections = ((engine_config.aggregator_window_secs / engine_config.inference_interval_secs) / 2).max(1) as usize;
|
||||
let aggregator = Mutex::new(AttackAggregator::new(engine_config.aggregator_window_secs, min_detections));
|
||||
|
||||
let max_flows_per_thread = engine_config.max_flows / num_threads.max(1) as usize;
|
||||
|
||||
let max_flows_per_thread = engine_config.max_flows / (num_threads as usize).max(1);
|
||||
let trackers: Vec<ThreadTracker> = (0..num_threads)
|
||||
.map(|_| Arc::new(Mutex::new(FlowTracker::new(max_flows_per_thread))))
|
||||
.collect();
|
||||
@ -61,7 +58,6 @@ impl Engine {
|
||||
inference_pipeline,
|
||||
aggregator,
|
||||
ml_alert,
|
||||
max_flows_per_thread,
|
||||
min_packets: engine_config.min_packets,
|
||||
batch_size: engine_config.batch_size,
|
||||
inference_interval_secs: engine_config.inference_interval_secs,
|
||||
@ -70,14 +66,23 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// xsk_manager calls this per queue_id; with symmetric hash each queue has its own tracker.
|
||||
pub fn tracker(&self, queue_id: u32) -> &ThreadTracker {
|
||||
&self.trackers[queue_id as usize]
|
||||
&self.trackers[queue_id as usize % self.trackers.len()]
|
||||
}
|
||||
|
||||
pub fn trackers(&self) -> &[ThreadTracker] {
|
||||
&self.trackers
|
||||
}
|
||||
|
||||
pub fn inference_interval_secs(&self) -> u64 {
|
||||
self.inference_interval_secs
|
||||
}
|
||||
|
||||
pub fn has_traffic_logger(&self) -> bool {
|
||||
self.traffic_logger.is_some()
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>) -> oneshot::Sender<()> {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
@ -100,18 +105,23 @@ impl Engine {
|
||||
}
|
||||
|
||||
fn run_inference_tick(&self) {
|
||||
// Collect flows from all per-thread trackers.
|
||||
// Each lock is held only for the duration of get_flows_for_inference (~microseconds).
|
||||
// XSK threads are barely impacted since they process on different trackers.
|
||||
let mut all_flows = Vec::new();
|
||||
let mut all_snapshots = Vec::new();
|
||||
let mut total_count = 0;
|
||||
|
||||
// Phase 1: O(1) lock per tracker — just swap
|
||||
for tracker in &self.trackers {
|
||||
let t = tracker.lock();
|
||||
let mut t = tracker.lock();
|
||||
total_count += t.flow_count();
|
||||
all_flows.extend(t.get_flows_for_inference(self.min_packets));
|
||||
all_snapshots.push(t.take_snapshot());
|
||||
// lock released here
|
||||
}
|
||||
|
||||
// Phase 2: filter outside all locks — O(flows) but non-blocking
|
||||
let all_flows: Vec<FlowData> = all_snapshots.into_iter()
|
||||
.flat_map(|map| map.into_values())
|
||||
.filter(|flow| flow.packet_count() >= self.min_packets)
|
||||
.collect();
|
||||
|
||||
log!(MLLog::FlowStats(
|
||||
total_count,
|
||||
all_flows.len(),
|
||||
@ -128,10 +138,6 @@ impl Engine {
|
||||
} else {
|
||||
self.run_inference(&all_flows);
|
||||
}
|
||||
|
||||
for tracker in &self.trackers {
|
||||
tracker.lock().cleanup_old_flows(self.flow_timeout_us);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_traffic(&self, flows: &[FlowData], logger: &TrafficLogger) {
|
||||
@ -190,14 +196,4 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by XSK threads. Lock is per-thread, uncontended on hot path.
|
||||
pub fn process_packet(tracker: &Mutex<FlowTracker>, packet_data: &[u8], is_ingress: bool) {
|
||||
match parse_packet(packet_data) {
|
||||
Some((packet_info, payload_start)) => {
|
||||
let payload = packet_data.get(payload_start..).unwrap_or(&[]);
|
||||
tracker.lock().process_packet(packet_info, is_ingress, payload);
|
||||
}
|
||||
None => log!(MLLog::ParsePacketFailed(packet_data.len())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,185 +13,18 @@ pub struct FlowFeatures {
|
||||
|
||||
impl FlowFeatures {
|
||||
pub fn extract(flow: &FlowData, feature_names: &[String]) -> Self {
|
||||
let precomputed = PrecomputedStats::compute(flow);
|
||||
let feature_num = feature_names.len();
|
||||
let mut features = Vec::with_capacity(feature_num);
|
||||
|
||||
for name in feature_names {
|
||||
let value = Self::get_feature_by_name(flow, name.trim());
|
||||
let value = precomputed.get(name.trim());
|
||||
features.push(value);
|
||||
}
|
||||
|
||||
Self { features, feature_num }
|
||||
}
|
||||
|
||||
fn get_feature_by_name(flow: &FlowData, feature_name: &str) -> f64 {
|
||||
let safe_div = |a: f64, b: f64| if b > 0.0 { a / b } else { 0.0 };
|
||||
|
||||
// 1-5
|
||||
let fwd_count = flow.fwd_packets.len() as f64;
|
||||
let bwd_count = flow.bwd_packets.len() as f64;
|
||||
let total_count = fwd_count + bwd_count;
|
||||
|
||||
let duration_us = flow.duration_us() as f64;
|
||||
let duration_s = duration_us / 1_000_000.0;
|
||||
let duration_s = if duration_s > 0.0 { duration_s } else { 1e-6 };
|
||||
|
||||
// 6-9
|
||||
let fwd_lengths: Vec<f64> = flow.fwd_packets.iter().map(|p| p.length as f64).collect();
|
||||
let (fwd_max, fwd_min, fwd_mean, fwd_std) = compute_stats(&fwd_lengths);
|
||||
|
||||
// 10-13
|
||||
let bwd_lengths: Vec<f64> = flow.bwd_packets.iter().map(|p| p.length as f64).collect();
|
||||
let (bwd_max, bwd_min, bwd_mean, bwd_std) = compute_stats(&bwd_lengths);
|
||||
|
||||
// 14-15
|
||||
let total_bytes = (flow.fwd_total_bytes + flow.bwd_total_bytes) as f64;
|
||||
|
||||
// 16-19
|
||||
let flow_iats = compute_flow_iats(&flow.fwd_packets, &flow.bwd_packets);
|
||||
let (flow_iat_max, flow_iat_min, flow_iat_mean, flow_iat_std) = compute_stats(&flow_iats);
|
||||
|
||||
// 20-24
|
||||
let fwd_iats = compute_iats(&flow.fwd_packets);
|
||||
let fwd_iat_total: f64 = fwd_iats.iter().sum();
|
||||
let (fwd_iat_max, fwd_iat_min, fwd_iat_mean, fwd_iat_std) = compute_stats(&fwd_iats);
|
||||
|
||||
// 25-29
|
||||
let bwd_iats = compute_iats(&flow.bwd_packets);
|
||||
let bwd_iat_total: f64 = bwd_iats.iter().sum();
|
||||
let (bwd_iat_max, bwd_iat_min, bwd_iat_mean, bwd_iat_std) = compute_stats(&bwd_iats);
|
||||
|
||||
// 30-37
|
||||
let fwd_psh = flow.fwd_packets.iter().filter(|p| p.flags & TCP_PSH != 0).count() as f64;
|
||||
let bwd_psh = flow.bwd_packets.iter().filter(|p| p.flags & TCP_PSH != 0).count() as f64;
|
||||
let fwd_urg = flow.fwd_packets.iter().filter(|p| p.flags & TCP_URG != 0).count() as f64;
|
||||
let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags & TCP_URG != 0).count() as f64;
|
||||
|
||||
// 38-55
|
||||
let all_lengths: Vec<f64> = flow
|
||||
.fwd_packets
|
||||
.iter()
|
||||
.chain(flow.bwd_packets.iter())
|
||||
.map(|p| p.length as f64)
|
||||
.collect();
|
||||
|
||||
let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths);
|
||||
|
||||
// 56-67
|
||||
let fwd_bulk = &flow.fwd_bulk_state;
|
||||
let bwd_bulk = &flow.bwd_bulk_state;
|
||||
|
||||
// 68-69
|
||||
let fwd_seg_sizes: Vec<f64> = flow
|
||||
.fwd_packets
|
||||
.iter()
|
||||
.filter(|p| p.payload_length > 0)
|
||||
.map(|p| p.payload_length as f64)
|
||||
.collect();
|
||||
|
||||
// 70-73
|
||||
let (active_max, active_min, active_mean, active_std) =
|
||||
compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
||||
|
||||
// 74-77
|
||||
let (idle_max, idle_min, idle_mean, idle_std) =
|
||||
compute_stats(&flow.idle_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
||||
|
||||
match feature_name {
|
||||
"Destination Port" | "Dst Port" | "dst_port" => flow.flow_key.dst_port as f64,
|
||||
"Protocol" | "protocol" => flow.flow_key.protocol as f64,
|
||||
"Flow Duration" | "flow_duration" => duration_us,
|
||||
"Total Fwd Packets" | "Tot Fwd Pkts" | "fwd_packets" => fwd_count,
|
||||
"Total Backward Packets" | "Tot Bwd Pkts" | "bwd_packets" => bwd_count,
|
||||
"Total Length of Fwd Packets" | "TotLen Fwd Pkts" | "fwd_bytes" => flow.fwd_total_bytes as f64,
|
||||
"Total Length of Bwd Packets" | "TotLen Bwd Pkts" | "bwd_bytes" => flow.bwd_total_bytes as f64,
|
||||
"Fwd Packet Length Max" => fwd_max,
|
||||
"Fwd Packet Length Min" => fwd_min,
|
||||
"Fwd Packet Length Mean" | "Fwd Pkt Len Mean" | "fwd_pkt_len_mean" => fwd_mean,
|
||||
"Fwd Packet Length Std" | "Fwd Pkt Len Std" | "fwd_pkt_len_std" => fwd_std,
|
||||
"Bwd Packet Length Max" => bwd_max,
|
||||
"Bwd Packet Length Min" => bwd_min,
|
||||
"Bwd Packet Length Mean" | "Bwd Pkt Len Mean" | "bwd_pkt_len_mean" => bwd_mean,
|
||||
"Bwd Packet Length Std" | "Bwd Pkt Len Std" | "bwd_pkt_len_std" => bwd_std,
|
||||
"Flow Bytes/s" | "Flow Byts/s" | "flow_bytes_per_sec" => safe_div(total_bytes, duration_s),
|
||||
"Flow Packets/s" | "Flow Pkts/s" | "flow_pkts_per_sec" => safe_div(total_count, duration_s),
|
||||
"Flow IAT Mean" | "flow_iat_mean" => flow_iat_mean,
|
||||
"Flow IAT Std" => flow_iat_std,
|
||||
"Flow IAT Max" => flow_iat_max,
|
||||
"Flow IAT Min" => flow_iat_min,
|
||||
"Fwd IAT Total" => fwd_iat_total,
|
||||
"Fwd IAT Mean" | "fwd_iat_mean" => fwd_iat_mean,
|
||||
"Fwd IAT Std" => fwd_iat_std,
|
||||
"Fwd IAT Max" => fwd_iat_max,
|
||||
"Fwd IAT Min" => fwd_iat_min,
|
||||
"Bwd IAT Total" => bwd_iat_total,
|
||||
"Bwd IAT Mean" | "bwd_iat_mean" => bwd_iat_mean,
|
||||
"Bwd IAT Std" => bwd_iat_std,
|
||||
"Bwd IAT Max" => bwd_iat_max,
|
||||
"Bwd IAT Min" => bwd_iat_min,
|
||||
"Fwd PSH Flags" => fwd_psh,
|
||||
"Bwd PSH Flags" => bwd_psh,
|
||||
"Fwd URG Flags" => fwd_urg,
|
||||
"Bwd URG Flags" => bwd_urg,
|
||||
"Fwd Header Length" => flow.fwd_header_bytes as f64,
|
||||
"Bwd Header Length" => flow.bwd_header_bytes as f64,
|
||||
"Fwd Packets/s" => safe_div(fwd_count, duration_s),
|
||||
"Bwd Packets/s" => safe_div(bwd_count, duration_s),
|
||||
"Min Packet Length" => min_len,
|
||||
"Max Packet Length" => max_len,
|
||||
"Packet Length Mean" | "Pkt Len Mean" | "pkt_len_mean" => mean_len,
|
||||
"Packet Length Std" | "Pkt Len Std" | "pkt_len_std" => std_len,
|
||||
"Packet Length Variance" => std_len * std_len,
|
||||
"FIN Flag Count" | "FIN Flag Cnt" | "fin_flag_cnt" => flow.fin_count as f64,
|
||||
"SYN Flag Count" | "SYN Flag Cnt" | "syn_flag_cnt" => flow.syn_count as f64,
|
||||
"RST Flag Count" | "RST Flag Cnt" | "rst_flag_cnt" => flow.rst_count as f64,
|
||||
"PSH Flag Count" | "PSH Flag Cnt" | "psh_flag_cnt" => flow.psh_count as f64,
|
||||
"ACK Flag Count" | "ACK Flag Cnt" | "ack_flag_cnt" => flow.ack_count as f64,
|
||||
"URG Flag Count" => flow.urg_count as f64,
|
||||
"CWE Flag Count" => flow.cwe_count as f64,
|
||||
"ECE Flag Count" => flow.ece_count as f64,
|
||||
"Down/Up Ratio" => safe_div(bwd_count, fwd_count),
|
||||
"Average Packet Size" => safe_div(total_bytes, total_count),
|
||||
"Avg Fwd Segment Size" => safe_div(flow.fwd_total_bytes as f64, fwd_count),
|
||||
"Avg Bwd Segment Size" => safe_div(flow.bwd_total_bytes as f64, bwd_count),
|
||||
"Fwd Header Length.1" => flow.fwd_header_bytes as f64,
|
||||
"Fwd Avg Bytes/Bulk" => safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64),
|
||||
"Fwd Avg Packets/Bulk" => safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64),
|
||||
"Fwd Avg Bulk Rate" => safe_div(
|
||||
fwd_bulk.total_bytes as f64,
|
||||
fwd_bulk.total_duration_us as f64 / 1_000_000.0,
|
||||
),
|
||||
"Bwd Avg Bytes/Bulk" => safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64),
|
||||
"Bwd Avg Packets/Bulk" => safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64),
|
||||
"Bwd Avg Bulk Rate" => safe_div(
|
||||
bwd_bulk.total_bytes as f64,
|
||||
bwd_bulk.total_duration_us as f64 / 1_000_000.0,
|
||||
),
|
||||
"Subflow Fwd Packets" => fwd_count,
|
||||
"Subflow Fwd Bytes" => flow.fwd_total_bytes as f64,
|
||||
"Subflow Bwd Packets" => bwd_count,
|
||||
"Subflow Bwd Bytes" => flow.bwd_total_bytes as f64,
|
||||
"Init_Win_bytes_forward" | "Init Fwd Win Byts" | "fwd_win_bytes" => flow.init_win_bytes_fwd as f64,
|
||||
"Init_Win_bytes_backward" | "Init Bwd Win Byts" | "bwd_win_bytes" => flow.init_win_bytes_bwd as f64,
|
||||
"act_data_pkt_fwd" | "Fwd Act Data Pkts" | "fwd_act_data_pkts" => fwd_seg_sizes.len() as f64,
|
||||
"min_seg_size_forward" | "Fwd Seg Size Min" | "fwd_seg_size_min" => fwd_seg_sizes
|
||||
.iter()
|
||||
.min_by(|a, b| a.total_cmp(b))
|
||||
.copied()
|
||||
.unwrap_or(0.0),
|
||||
"Active Mean" => active_mean,
|
||||
"Active Std" => active_std,
|
||||
"Active Max" => active_max,
|
||||
"Active Min" => active_min,
|
||||
"Idle Mean" => idle_mean,
|
||||
"Idle Std" => idle_std,
|
||||
"Idle Max" => idle_max,
|
||||
"Idle Min" => idle_min,
|
||||
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize(&mut self, means: &[f64], stds: &[f64]) {
|
||||
for i in 0..self.feature_num {
|
||||
if stds[i] > 0.0 {
|
||||
@ -313,6 +146,350 @@ impl FlowFeatures {
|
||||
}
|
||||
}
|
||||
|
||||
/// All statistics pre-computed once from a FlowData, then looked up by feature name.
|
||||
struct PrecomputedStats {
|
||||
// Basic counts and durations
|
||||
dst_port: f64,
|
||||
protocol: f64,
|
||||
duration_us: f64,
|
||||
fwd_count: f64,
|
||||
bwd_count: f64,
|
||||
total_count: f64,
|
||||
fwd_total_bytes: f64,
|
||||
bwd_total_bytes: f64,
|
||||
total_bytes: f64,
|
||||
duration_s: f64,
|
||||
|
||||
// Forward packet length stats
|
||||
fwd_len_max: f64,
|
||||
fwd_len_min: f64,
|
||||
fwd_len_mean: f64,
|
||||
fwd_len_std: f64,
|
||||
|
||||
// Backward packet length stats
|
||||
bwd_len_max: f64,
|
||||
bwd_len_min: f64,
|
||||
bwd_len_mean: f64,
|
||||
bwd_len_std: f64,
|
||||
|
||||
// Combined packet length stats
|
||||
all_len_max: f64,
|
||||
all_len_min: f64,
|
||||
all_len_mean: f64,
|
||||
all_len_std: f64,
|
||||
|
||||
// Flow IAT stats
|
||||
flow_iat_max: f64,
|
||||
flow_iat_min: f64,
|
||||
flow_iat_mean: f64,
|
||||
flow_iat_std: f64,
|
||||
|
||||
// Forward IAT stats
|
||||
fwd_iat_total: f64,
|
||||
fwd_iat_max: f64,
|
||||
fwd_iat_min: f64,
|
||||
fwd_iat_mean: f64,
|
||||
fwd_iat_std: f64,
|
||||
|
||||
// Backward IAT stats
|
||||
bwd_iat_total: f64,
|
||||
bwd_iat_max: f64,
|
||||
bwd_iat_min: f64,
|
||||
bwd_iat_mean: f64,
|
||||
bwd_iat_std: f64,
|
||||
|
||||
// Flag counts (per-direction)
|
||||
fwd_psh: f64,
|
||||
bwd_psh: f64,
|
||||
fwd_urg: f64,
|
||||
bwd_urg: f64,
|
||||
|
||||
// Header bytes
|
||||
fwd_header_bytes: f64,
|
||||
bwd_header_bytes: f64,
|
||||
|
||||
// Flag counts (global)
|
||||
fin_count: f64,
|
||||
syn_count: f64,
|
||||
rst_count: f64,
|
||||
psh_count: f64,
|
||||
ack_count: f64,
|
||||
urg_count: f64,
|
||||
cwe_count: f64,
|
||||
ece_count: f64,
|
||||
|
||||
// Bulk stats
|
||||
fwd_avg_bytes_bulk: f64,
|
||||
fwd_avg_packets_bulk: f64,
|
||||
fwd_avg_bulk_rate: f64,
|
||||
bwd_avg_bytes_bulk: f64,
|
||||
bwd_avg_packets_bulk: f64,
|
||||
bwd_avg_bulk_rate: f64,
|
||||
|
||||
// Window sizes
|
||||
init_win_bytes_fwd: f64,
|
||||
init_win_bytes_bwd: f64,
|
||||
|
||||
// Active data packets
|
||||
act_data_pkt_fwd: f64,
|
||||
|
||||
// Min forward header (segment) size
|
||||
min_seg_size_forward: f64,
|
||||
|
||||
// Active/idle period stats
|
||||
active_max: f64,
|
||||
active_min: f64,
|
||||
active_mean: f64,
|
||||
active_std: f64,
|
||||
idle_max: f64,
|
||||
idle_min: f64,
|
||||
idle_mean: f64,
|
||||
idle_std: f64,
|
||||
}
|
||||
|
||||
impl PrecomputedStats {
|
||||
fn compute(flow: &FlowData) -> Self {
|
||||
let safe_div = |a: f64, b: f64| if b > 0.0 { a / b } else { 0.0 };
|
||||
|
||||
let fwd_count = flow.fwd_packets.len() as f64;
|
||||
let bwd_count = flow.bwd_packets.len() as f64;
|
||||
let total_count = fwd_count + bwd_count;
|
||||
|
||||
let duration_us = flow.duration_us() as f64;
|
||||
let duration_s = {
|
||||
let s = duration_us / 1_000_000.0;
|
||||
if s > 0.0 { s } else { 1e-6 }
|
||||
};
|
||||
|
||||
let fwd_total_bytes = flow.fwd_total_bytes as f64;
|
||||
let bwd_total_bytes = flow.bwd_total_bytes as f64;
|
||||
let total_bytes = fwd_total_bytes + bwd_total_bytes;
|
||||
|
||||
// Packet length stats
|
||||
let fwd_lengths: Vec<f64> = flow.fwd_packets.iter().map(|p| p.payload_length as f64).collect();
|
||||
let (fwd_len_max, fwd_len_min, fwd_len_mean, fwd_len_std) = compute_stats(&fwd_lengths);
|
||||
|
||||
let bwd_lengths: Vec<f64> = flow.bwd_packets.iter().map(|p| p.payload_length as f64).collect();
|
||||
let (bwd_len_max, bwd_len_min, bwd_len_mean, bwd_len_std) = compute_stats(&bwd_lengths);
|
||||
|
||||
let all_lengths: Vec<f64> = flow
|
||||
.fwd_packets
|
||||
.iter()
|
||||
.chain(flow.bwd_packets.iter())
|
||||
.map(|p| p.payload_length as f64)
|
||||
.collect();
|
||||
let (all_len_max, all_len_min, all_len_mean, all_len_std) = compute_stats(&all_lengths);
|
||||
|
||||
// IAT stats
|
||||
let flow_iats = compute_flow_iats(&flow.fwd_packets, &flow.bwd_packets);
|
||||
let (flow_iat_max, flow_iat_min, flow_iat_mean, flow_iat_std) = compute_stats(&flow_iats);
|
||||
|
||||
let fwd_iats = compute_iats(&flow.fwd_packets);
|
||||
let fwd_iat_total: f64 = fwd_iats.iter().sum();
|
||||
let (fwd_iat_max, fwd_iat_min, fwd_iat_mean, fwd_iat_std) = compute_stats(&fwd_iats);
|
||||
|
||||
let bwd_iats = compute_iats(&flow.bwd_packets);
|
||||
let bwd_iat_total: f64 = bwd_iats.iter().sum();
|
||||
let (bwd_iat_max, bwd_iat_min, bwd_iat_mean, bwd_iat_std) = compute_stats(&bwd_iats);
|
||||
|
||||
// Per-direction flag counts
|
||||
let fwd_psh = flow.fwd_packets.iter().filter(|p| p.flags & TCP_PSH != 0).count() as f64;
|
||||
let bwd_psh = flow.bwd_packets.iter().filter(|p| p.flags & TCP_PSH != 0).count() as f64;
|
||||
let fwd_urg = flow.fwd_packets.iter().filter(|p| p.flags & TCP_URG != 0).count() as f64;
|
||||
let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags & TCP_URG != 0).count() as f64;
|
||||
|
||||
// Bulk stats
|
||||
let fwd_bulk = &flow.fwd_bulk_state;
|
||||
let bwd_bulk = &flow.bwd_bulk_state;
|
||||
|
||||
let fwd_avg_bytes_bulk = safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64);
|
||||
let fwd_avg_packets_bulk = safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64);
|
||||
let fwd_avg_bulk_rate = safe_div(
|
||||
fwd_bulk.total_bytes as f64,
|
||||
fwd_bulk.total_duration_us as f64 / 1_000_000.0,
|
||||
);
|
||||
let bwd_avg_bytes_bulk = safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64);
|
||||
let bwd_avg_packets_bulk = safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64);
|
||||
let bwd_avg_bulk_rate = safe_div(
|
||||
bwd_bulk.total_bytes as f64,
|
||||
bwd_bulk.total_duration_us as f64 / 1_000_000.0,
|
||||
);
|
||||
|
||||
// Min forward segment (header) size
|
||||
let min_seg_size_forward = flow
|
||||
.fwd_packets
|
||||
.iter()
|
||||
.map(|p| p.header_length as f64)
|
||||
.min_by(|a, b| a.total_cmp(b))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Active/idle period stats
|
||||
let (active_max, active_min, active_mean, active_std) =
|
||||
compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
||||
let (idle_max, idle_min, idle_mean, idle_std) =
|
||||
compute_stats(&flow.idle_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
||||
|
||||
Self {
|
||||
dst_port: flow.flow_key.dst_port as f64,
|
||||
protocol: flow.flow_key.protocol as f64,
|
||||
duration_us,
|
||||
fwd_count,
|
||||
bwd_count,
|
||||
total_count,
|
||||
fwd_total_bytes,
|
||||
bwd_total_bytes,
|
||||
total_bytes,
|
||||
duration_s,
|
||||
fwd_len_max,
|
||||
fwd_len_min,
|
||||
fwd_len_mean,
|
||||
fwd_len_std,
|
||||
bwd_len_max,
|
||||
bwd_len_min,
|
||||
bwd_len_mean,
|
||||
bwd_len_std,
|
||||
all_len_max,
|
||||
all_len_min,
|
||||
all_len_mean,
|
||||
all_len_std,
|
||||
flow_iat_max,
|
||||
flow_iat_min,
|
||||
flow_iat_mean,
|
||||
flow_iat_std,
|
||||
fwd_iat_total,
|
||||
fwd_iat_max,
|
||||
fwd_iat_min,
|
||||
fwd_iat_mean,
|
||||
fwd_iat_std,
|
||||
bwd_iat_total,
|
||||
bwd_iat_max,
|
||||
bwd_iat_min,
|
||||
bwd_iat_mean,
|
||||
bwd_iat_std,
|
||||
fwd_psh,
|
||||
bwd_psh,
|
||||
fwd_urg,
|
||||
bwd_urg,
|
||||
fwd_header_bytes: flow.fwd_header_bytes as f64,
|
||||
bwd_header_bytes: flow.bwd_header_bytes as f64,
|
||||
fin_count: flow.fin_count as f64,
|
||||
syn_count: flow.syn_count as f64,
|
||||
rst_count: flow.rst_count as f64,
|
||||
psh_count: flow.psh_count as f64,
|
||||
ack_count: flow.ack_count as f64,
|
||||
urg_count: flow.urg_count as f64,
|
||||
cwe_count: flow.cwe_count as f64,
|
||||
ece_count: flow.ece_count as f64,
|
||||
fwd_avg_bytes_bulk,
|
||||
fwd_avg_packets_bulk,
|
||||
fwd_avg_bulk_rate,
|
||||
bwd_avg_bytes_bulk,
|
||||
bwd_avg_packets_bulk,
|
||||
bwd_avg_bulk_rate,
|
||||
init_win_bytes_fwd: flow.init_win_bytes_fwd as f64,
|
||||
init_win_bytes_bwd: flow.init_win_bytes_bwd as f64,
|
||||
act_data_pkt_fwd: flow.act_data_pkt_fwd as f64,
|
||||
min_seg_size_forward,
|
||||
active_max,
|
||||
active_min,
|
||||
active_mean,
|
||||
active_std,
|
||||
idle_max,
|
||||
idle_min,
|
||||
idle_mean,
|
||||
idle_std,
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, feature_name: &str) -> f64 {
|
||||
let safe_div = |a: f64, b: f64| if b > 0.0 { a / b } else { 0.0 };
|
||||
|
||||
match feature_name {
|
||||
"Destination Port" | "Dst Port" | "dst_port" => self.dst_port,
|
||||
"Protocol" | "protocol" => self.protocol,
|
||||
"Flow Duration" | "flow_duration" => self.duration_us,
|
||||
"Total Fwd Packets" | "Tot Fwd Pkts" | "fwd_packets" => self.fwd_count,
|
||||
"Total Backward Packets" | "Tot Bwd Pkts" | "bwd_packets" => self.bwd_count,
|
||||
"Total Length of Fwd Packets" | "TotLen Fwd Pkts" | "fwd_bytes" => self.fwd_total_bytes,
|
||||
"Total Length of Bwd Packets" | "TotLen Bwd Pkts" | "bwd_bytes" => self.bwd_total_bytes,
|
||||
"Fwd Packet Length Max" => self.fwd_len_max,
|
||||
"Fwd Packet Length Min" => self.fwd_len_min,
|
||||
"Fwd Packet Length Mean" | "Fwd Pkt Len Mean" | "fwd_pkt_len_mean" => self.fwd_len_mean,
|
||||
"Fwd Packet Length Std" | "Fwd Pkt Len Std" | "fwd_pkt_len_std" => self.fwd_len_std,
|
||||
"Bwd Packet Length Max" => self.bwd_len_max,
|
||||
"Bwd Packet Length Min" => self.bwd_len_min,
|
||||
"Bwd Packet Length Mean" | "Bwd Pkt Len Mean" | "bwd_pkt_len_mean" => self.bwd_len_mean,
|
||||
"Bwd Packet Length Std" | "Bwd Pkt Len Std" | "bwd_pkt_len_std" => self.bwd_len_std,
|
||||
"Flow Bytes/s" | "Flow Byts/s" | "flow_bytes_per_sec" => safe_div(self.total_bytes, self.duration_s),
|
||||
"Flow Packets/s" | "Flow Pkts/s" | "flow_pkts_per_sec" => safe_div(self.total_count, self.duration_s),
|
||||
"Flow IAT Mean" | "flow_iat_mean" => self.flow_iat_mean,
|
||||
"Flow IAT Std" => self.flow_iat_std,
|
||||
"Flow IAT Max" => self.flow_iat_max,
|
||||
"Flow IAT Min" => self.flow_iat_min,
|
||||
"Fwd IAT Total" => self.fwd_iat_total,
|
||||
"Fwd IAT Mean" | "fwd_iat_mean" => self.fwd_iat_mean,
|
||||
"Fwd IAT Std" => self.fwd_iat_std,
|
||||
"Fwd IAT Max" => self.fwd_iat_max,
|
||||
"Fwd IAT Min" => self.fwd_iat_min,
|
||||
"Bwd IAT Total" => self.bwd_iat_total,
|
||||
"Bwd IAT Mean" | "bwd_iat_mean" => self.bwd_iat_mean,
|
||||
"Bwd IAT Std" => self.bwd_iat_std,
|
||||
"Bwd IAT Max" => self.bwd_iat_max,
|
||||
"Bwd IAT Min" => self.bwd_iat_min,
|
||||
"Fwd PSH Flags" => self.fwd_psh,
|
||||
"Bwd PSH Flags" => self.bwd_psh,
|
||||
"Fwd URG Flags" => self.fwd_urg,
|
||||
"Bwd URG Flags" => self.bwd_urg,
|
||||
"Fwd Header Length" => self.fwd_header_bytes,
|
||||
"Bwd Header Length" => self.bwd_header_bytes,
|
||||
"Fwd Packets/s" => safe_div(self.fwd_count, self.duration_s),
|
||||
"Bwd Packets/s" => safe_div(self.bwd_count, self.duration_s),
|
||||
"Min Packet Length" => self.all_len_min,
|
||||
"Max Packet Length" => self.all_len_max,
|
||||
"Packet Length Mean" | "Pkt Len Mean" | "pkt_len_mean" => self.all_len_mean,
|
||||
"Packet Length Std" | "Pkt Len Std" | "pkt_len_std" => self.all_len_std,
|
||||
"Packet Length Variance" => self.all_len_std * self.all_len_std,
|
||||
"FIN Flag Count" | "FIN Flag Cnt" | "fin_flag_cnt" => self.fin_count,
|
||||
"SYN Flag Count" | "SYN Flag Cnt" | "syn_flag_cnt" => self.syn_count,
|
||||
"RST Flag Count" | "RST Flag Cnt" | "rst_flag_cnt" => self.rst_count,
|
||||
"PSH Flag Count" | "PSH Flag Cnt" | "psh_flag_cnt" => self.psh_count,
|
||||
"ACK Flag Count" | "ACK Flag Cnt" | "ack_flag_cnt" => self.ack_count,
|
||||
"URG Flag Count" => self.urg_count,
|
||||
"CWE Flag Count" => self.cwe_count,
|
||||
"ECE Flag Count" => self.ece_count,
|
||||
"Down/Up Ratio" => safe_div(self.bwd_count, self.fwd_count),
|
||||
"Average Packet Size" => safe_div(self.total_bytes, self.total_count),
|
||||
"Avg Fwd Segment Size" => safe_div(self.fwd_total_bytes, self.fwd_count),
|
||||
"Avg Bwd Segment Size" => safe_div(self.bwd_total_bytes, self.bwd_count),
|
||||
"Fwd Header Length.1" => self.fwd_header_bytes,
|
||||
"Fwd Avg Bytes/Bulk" => self.fwd_avg_bytes_bulk,
|
||||
"Fwd Avg Packets/Bulk" => self.fwd_avg_packets_bulk,
|
||||
"Fwd Avg Bulk Rate" => self.fwd_avg_bulk_rate,
|
||||
"Bwd Avg Bytes/Bulk" => self.bwd_avg_bytes_bulk,
|
||||
"Bwd Avg Packets/Bulk" => self.bwd_avg_packets_bulk,
|
||||
"Bwd Avg Bulk Rate" => self.bwd_avg_bulk_rate,
|
||||
"Subflow Fwd Packets" => self.fwd_count,
|
||||
"Subflow Fwd Bytes" => self.fwd_total_bytes,
|
||||
"Subflow Bwd Packets" => self.bwd_count,
|
||||
"Subflow Bwd Bytes" => self.bwd_total_bytes,
|
||||
"Init_Win_bytes_forward" | "Init Fwd Win Byts" | "fwd_win_bytes" => self.init_win_bytes_fwd,
|
||||
"Init_Win_bytes_backward" | "Init Bwd Win Byts" | "bwd_win_bytes" => self.init_win_bytes_bwd,
|
||||
"act_data_pkt_fwd" | "Fwd Act Data Pkts" | "fwd_act_data_pkts" => self.act_data_pkt_fwd,
|
||||
"min_seg_size_forward" | "Fwd Seg Size Min" | "fwd_seg_size_min" => self.min_seg_size_forward,
|
||||
"Active Mean" => self.active_mean,
|
||||
"Active Std" => self.active_std,
|
||||
"Active Max" => self.active_max,
|
||||
"Active Min" => self.active_min,
|
||||
"Idle Mean" => self.idle_mean,
|
||||
"Idle Std" => self.idle_std,
|
||||
"Idle Max" => self.idle_max,
|
||||
"Idle Min" => self.idle_min,
|
||||
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_stats(values: &[f64]) -> (f64, f64, f64, f64) {
|
||||
if values.is_empty() {
|
||||
return (0.0, 0.0, 0.0, 0.0);
|
||||
@ -325,7 +502,11 @@ fn compute_stats(values: &[f64]) -> (f64, f64, f64, f64) {
|
||||
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
|
||||
let variance: f64 = values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n;
|
||||
let variance: f64 = if n > 1.0 {
|
||||
values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let std = variance.sqrt();
|
||||
|
||||
(max, min, mean, std)
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time;
|
||||
|
||||
use common::define::tcp_flags::*;
|
||||
|
||||
use crate::model::direction::Direction;
|
||||
@ -34,6 +32,8 @@ pub struct FlowData {
|
||||
pub last_packet_time: u64,
|
||||
pub fwd_bulk_state: BulkState,
|
||||
pub bwd_bulk_state: BulkState,
|
||||
pub act_data_pkt_fwd: u32,
|
||||
is_first_packet: bool,
|
||||
}
|
||||
|
||||
impl FlowData {
|
||||
@ -64,6 +64,8 @@ impl FlowData {
|
||||
last_packet_time: first_packet.timestamp_us,
|
||||
fwd_bulk_state: BulkState::default(),
|
||||
bwd_bulk_state: BulkState::default(),
|
||||
act_data_pkt_fwd: 0,
|
||||
is_first_packet: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,11 +102,17 @@ impl FlowData {
|
||||
self.last_packet_time = packet.timestamp_us;
|
||||
self.last_time_us = packet.timestamp_us;
|
||||
|
||||
if self.is_first_packet {
|
||||
self.is_first_packet = false;
|
||||
} else if packet.is_forward && packet.payload_length > 0 {
|
||||
self.act_data_pkt_fwd += 1;
|
||||
}
|
||||
|
||||
if packet.is_forward {
|
||||
if self.fwd_packets.len() < MAX_PACKETS_PER_DIRECTION {
|
||||
self.fwd_packets.push(packet_data.clone());
|
||||
}
|
||||
self.fwd_total_bytes += packet.packet_length as u64;
|
||||
self.fwd_total_bytes += packet.payload_length as u64;
|
||||
self.fwd_header_bytes += packet.header_length as u64;
|
||||
if self.init_win_bytes_fwd == 0 { self.init_win_bytes_fwd = packet.tcp_window_size; }
|
||||
Self::update_bulk_state(&mut self.fwd_bulk_state, &packet_data);
|
||||
@ -112,7 +120,7 @@ impl FlowData {
|
||||
if self.bwd_packets.len() < MAX_PACKETS_PER_DIRECTION {
|
||||
self.bwd_packets.push(packet_data.clone());
|
||||
}
|
||||
self.bwd_total_bytes += packet.packet_length as u64;
|
||||
self.bwd_total_bytes += packet.payload_length as u64;
|
||||
self.bwd_header_bytes += packet.header_length as u64;
|
||||
if self.init_win_bytes_bwd == 0 { self.init_win_bytes_bwd = packet.tcp_window_size; }
|
||||
Self::update_bulk_state(&mut self.bwd_bulk_state, &packet_data);
|
||||
@ -167,127 +175,87 @@ impl FlowData {
|
||||
/// Per-thread flow tracker. No locks — each XSK thread owns one.
|
||||
/// RSS guarantees the same flow always goes to the same thread.
|
||||
pub struct FlowTracker {
|
||||
flows: HashMap<FlowKey, FlowData>,
|
||||
active: HashMap<FlowKey, FlowData>,
|
||||
max_flows: usize,
|
||||
}
|
||||
|
||||
impl FlowTracker {
|
||||
pub fn new(max_flows: usize) -> Self {
|
||||
Self {
|
||||
flows: HashMap::new(),
|
||||
active: HashMap::new(),
|
||||
max_flows,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_packet(&mut self, mut packet: UserPacket, is_ingress: bool, payload: &[u8]) {
|
||||
let direction = if is_ingress { Direction::Ingress } else { Direction::Egress };
|
||||
/// Swap active flows with an empty map and return the old one.
|
||||
/// This is O(1) — the caller filters outside the lock.
|
||||
pub fn take_snapshot(&mut self) -> HashMap<FlowKey, FlowData> {
|
||||
let mut snapshot = HashMap::with_capacity(self.active.capacity());
|
||||
std::mem::swap(&mut self.active, &mut snapshot);
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub fn process_packet(&mut self, mut packet: UserPacket, is_ingress: bool) {
|
||||
let packet_key = FlowKey::from_packet(&packet);
|
||||
let proto = packet_key.protocol;
|
||||
let src_port = packet_key.src_port;
|
||||
let dst_port = packet_key.dst_port;
|
||||
let reversed_key = packet_key.clone().reverse();
|
||||
|
||||
let (actual_key, is_forward) = if self.flows.contains_key(&packet_key) {
|
||||
// Try to match an existing flow first (canonical key already established).
|
||||
let (actual_key, is_forward) = if self.active.contains_key(&packet_key) {
|
||||
(packet_key, true)
|
||||
} else if self.flows.contains_key(&reversed_key) {
|
||||
} else if self.active.contains_key(&reversed_key) {
|
||||
(reversed_key, false)
|
||||
} else {
|
||||
let has_syn = packet.tcp_flags & TCP_SYN != 0;
|
||||
let has_ack = packet.tcp_flags & TCP_ACK != 0;
|
||||
if has_syn && has_ack {
|
||||
// New flow: determine initiator using TCP flags, fall back to is_ingress.
|
||||
let syn = packet.tcp_flags & TCP_SYN != 0;
|
||||
let ack = packet.tcp_flags & TCP_ACK != 0;
|
||||
if syn && ack {
|
||||
// SYN+ACK: sender is the responder.
|
||||
// Ingress: external server responding to internal client → reverse so
|
||||
// canonical key has internal client as src.
|
||||
// Egress: internal server responding to external client → keep as-is.
|
||||
if is_ingress { (reversed_key, false) } else { (packet_key, true) }
|
||||
} else if has_syn {
|
||||
} else if syn {
|
||||
// SYN: sender is always the initiator.
|
||||
(packet_key, true)
|
||||
} else {
|
||||
match detect_initiator(payload, proto, src_port, dst_port) {
|
||||
Some(true) => (packet_key, true),
|
||||
Some(false) => (reversed_key, false),
|
||||
None => (packet_key, true),
|
||||
}
|
||||
// Mid-stream / UDP / ICMP: use is_ingress as best-effort heuristic.
|
||||
// Egress = we are the initiator (forward); ingress = remote initiated (backward).
|
||||
if is_ingress { (reversed_key, false) } else { (packet_key, true) }
|
||||
}
|
||||
};
|
||||
|
||||
packet.is_forward = is_forward;
|
||||
let initiator_direction = if is_forward { direction } else { direction.flip() };
|
||||
|
||||
let flow = self.flows
|
||||
// Record which interface the initiator is on for this flow.
|
||||
let initiator_direction = if is_forward {
|
||||
if is_ingress { Direction::Ingress } else { Direction::Egress }
|
||||
} else {
|
||||
if is_ingress { Direction::Egress } else { Direction::Ingress }
|
||||
};
|
||||
|
||||
let flow = self.active
|
||||
.entry(actual_key.clone())
|
||||
.or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction));
|
||||
|
||||
flow.add_packet(&packet);
|
||||
|
||||
if self.flows.len() > self.max_flows {
|
||||
if let Some(oldest_key) = self.flows.iter()
|
||||
if self.active.len() > self.max_flows {
|
||||
if let Some(oldest_key) = self.active.iter()
|
||||
.min_by_key(|(_, flow)| flow.last_time_us)
|
||||
.map(|(k, _)| k.clone())
|
||||
{
|
||||
self.flows.remove(&oldest_key);
|
||||
self.active.remove(&oldest_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take all flows out, leaving this tracker empty. Lock-free.
|
||||
pub fn drain_flows(&mut self) -> Vec<FlowData> {
|
||||
self.flows.drain().map(|(_, v)| v).collect()
|
||||
}
|
||||
|
||||
/// Get a snapshot without draining.
|
||||
pub fn get_flows(&self) -> Vec<FlowData> {
|
||||
self.flows.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn get_flows_for_inference(&self, min_packets: usize) -> Vec<FlowData> {
|
||||
self.flows
|
||||
.values()
|
||||
.filter(|flow| flow.packet_count() >= min_packets)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn cleanup_old_flows(&mut self, max_age_us: u64) {
|
||||
let now = time::SystemTime::now()
|
||||
.duration_since(time::UNIX_EPOCH)
|
||||
.map(|d| d.as_micros() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
self.flows.retain(|_, flow| now.saturating_sub(flow.last_time_us) < max_age_us);
|
||||
self.active.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn flow_count(&self) -> usize {
|
||||
self.flows.len()
|
||||
self.active.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16) -> Option<bool> {
|
||||
if payload.is_empty() { return None; }
|
||||
|
||||
if payload.len() >= 6 && payload[0] == 0x16 {
|
||||
return match payload[5] {
|
||||
0x01 => Some(true),
|
||||
0x02 => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
if payload.len() >= 5 {
|
||||
if payload.starts_with(b"GET ")
|
||||
|| payload.starts_with(b"POST ")
|
||||
|| payload.starts_with(b"PUT ")
|
||||
|| payload.starts_with(b"HEAD ")
|
||||
|| payload.starts_with(b"DELETE ")
|
||||
|| payload.starts_with(b"OPTIONS ")
|
||||
|| payload.starts_with(b"PATCH ")
|
||||
{
|
||||
return Some(true);
|
||||
}
|
||||
if payload.starts_with(b"HTTP/") {
|
||||
return Some(false);
|
||||
}
|
||||
}
|
||||
|
||||
if protocol == 17 && (src_port == 53 || dst_port == 53) && payload.len() >= 3 {
|
||||
return Some((payload[2] >> 7) == 0);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@ -25,10 +25,8 @@ impl Inference {
|
||||
}
|
||||
|
||||
pub fn infer_single(&self, flow: &FlowData) -> Option<DetectionResult> {
|
||||
// extract
|
||||
let ae_features = self.preprocess_ae_features(flow);
|
||||
|
||||
// 2. Deep Autoencoder
|
||||
let ae_input = Self::vec_to_array2(&ae_features);
|
||||
let ae_score = match self.run_autoencoder(&ae_input) {
|
||||
Ok(score) => score,
|
||||
@ -77,7 +75,6 @@ impl Inference {
|
||||
features.winsorize(&self.config.ae_clip_params, &self.config.ae_feature_names);
|
||||
features.normalize(&self.config.ae_scaler_mean, &self.config.ae_scaler_std);
|
||||
features.clip(self.config.ae_post_clip_min, self.config.ae_post_clip_max);
|
||||
// Note: f64->f32 precision loss is acceptable for ML inference
|
||||
features.features.iter().map(|&x| x as f32).collect()
|
||||
}
|
||||
|
||||
@ -109,7 +106,7 @@ impl Inference {
|
||||
.into_dimensionality::<tract_ndarray::Ix2>()?;
|
||||
|
||||
let diff = input - &output;
|
||||
let mse = (&diff * &diff).sum() / output.len() as f32;
|
||||
let mse = (&diff * &diff).sum() / self.config.ae_feature_names.len() as f32;
|
||||
|
||||
Ok(mse)
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ impl MLModels {
|
||||
Ok(model.into_optimized()?.into_runnable()?)
|
||||
};
|
||||
|
||||
load().map_err(|_| MLError::ModelLoadFailed { path: model_path })
|
||||
load().map_err(|_| MLError::ModelLoadFailed(model_path))
|
||||
}
|
||||
|
||||
pub fn get_model_info(&self, name: &str) -> String {
|
||||
|
||||
@ -3,6 +3,9 @@ use std::io::{BufWriter, Write};
|
||||
use std::thread;
|
||||
|
||||
use crossbeam::channel::{bounded, Sender, TrySendError};
|
||||
use macros::log;
|
||||
|
||||
use crate::model::log::ml::MLLog;
|
||||
|
||||
pub struct TrafficLogger {
|
||||
sender: Sender<Vec<String>>,
|
||||
@ -27,7 +30,7 @@ impl TrafficLogger {
|
||||
.spawn(move || {
|
||||
for record in receiver {
|
||||
if let Err(e) = writeln!(writer, "{}", record.join(",")) {
|
||||
eprintln!("[traffic-logger] write error: {}", e);
|
||||
log!(MLLog::TrafficLogWriteError(e.to_string()));
|
||||
}
|
||||
}
|
||||
let _ = writer.flush();
|
||||
@ -38,8 +41,7 @@ impl TrafficLogger {
|
||||
|
||||
pub fn log_row(&self, record: Vec<String>) {
|
||||
if let Err(TrySendError::Disconnected(_)) = self.sender.try_send(record) {
|
||||
eprintln!("[traffic-logger] channel disconnected");
|
||||
log!(MLLog::TrafficLogChannelDisconnected);
|
||||
}
|
||||
// Full is ok - just drop the record
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +56,11 @@ impl System {
|
||||
|
||||
let inference_config = Arc::new(InferenceConfig::load_file(&app_config.inference.models_config_name)?);
|
||||
|
||||
// Write queue count to eBPF maps for symmetric hash redirect
|
||||
let num_queues = app_config.network.combined_queue_count;
|
||||
Self::write_num_queues(&mut ingress_ebpf, num_queues)?;
|
||||
Self::write_num_queues(&mut egress_ebpf, num_queues)?;
|
||||
|
||||
let ebpf_services = Arc::new(EbpfServices::new(
|
||||
app_config.clone(),
|
||||
&mut ingress_ebpf,
|
||||
@ -125,8 +130,8 @@ impl System {
|
||||
let egress_ifname = self.app_config.network.egress_ifname.clone();
|
||||
Self::set_memory_limit()?;
|
||||
|
||||
Self::attach_xdp(&mut self.ingress_ebpf, &ingress_ifname, true)?; // already loaded in configure_ingress_pipeline
|
||||
Self::attach_xdp(&mut self.egress_ebpf, &egress_ifname, false)?; // load now
|
||||
Self::attach_xdp(&mut self.ingress_ebpf, &ingress_ifname, true)?;
|
||||
Self::attach_xdp(&mut self.egress_ebpf, &egress_ifname, false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -149,10 +154,14 @@ impl System {
|
||||
let inference_config = self.inference_config.clone();
|
||||
let access_control = self.ebpf_services.access_control.clone();
|
||||
let protocol_filter = self.ebpf_services.protocol_filter.clone();
|
||||
let dns_filter = self.ebpf_services.dns_filter.clone();
|
||||
let geo_block = self.ebpf_services.geo_block.clone();
|
||||
let rate_limit = self.ebpf_services.rate_limit.clone();
|
||||
let health = self.app_services.health.clone();
|
||||
let ml_alert = self.app_services.ml_alert.clone();
|
||||
let ml_engine = self.app_services.ml_engine.clone();
|
||||
let flow_statistics = self.app_services.flow_statistics.clone();
|
||||
let drop_monitor = self.ebpf_services.drop_monitor.clone();
|
||||
let port = self.app_config.http.http_server_bind_port;
|
||||
HttpServer::new(move || {
|
||||
let cors = actix_cors::Cors::default()
|
||||
@ -167,10 +176,14 @@ impl System {
|
||||
.app_data(web::Data::from(inference_config.clone()))
|
||||
.app_data(web::Data::from(access_control.clone()))
|
||||
.app_data(web::Data::from(protocol_filter.clone()))
|
||||
.app_data(web::Data::from(dns_filter.clone()))
|
||||
.app_data(web::Data::from(geo_block.clone()))
|
||||
.app_data(web::Data::from(rate_limit.clone()))
|
||||
.app_data(web::Data::from(health.clone()))
|
||||
.app_data(web::Data::from(ml_alert.clone()))
|
||||
.app_data(web::Data::from(ml_engine.clone()))
|
||||
.app_data(web::Data::from(flow_statistics.clone()))
|
||||
.app_data(web::Data::from(drop_monitor.clone()))
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.service(acl::initialize())
|
||||
@ -209,7 +222,6 @@ impl System {
|
||||
) -> Result<ProgramArray<MapData>, Error> {
|
||||
let registry = stage_registry();
|
||||
|
||||
// Load entry point program BEFORE taking maps — verifier needs map fds at load time
|
||||
let entry: &mut Xdp = ebpf
|
||||
.program_mut("net_guardia")
|
||||
.ok_or(EbpfError::ProgramNotFound)?
|
||||
@ -217,41 +229,35 @@ impl System {
|
||||
.map_err(EbpfError::GetProgramFailed)?;
|
||||
entry.load().map_err(EbpfError::LoadProgramFailed)?;
|
||||
|
||||
// Take maps
|
||||
let pa_map = ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?;
|
||||
let mut program_array = ProgramArray::try_from(pa_map).map_err(EbpfError::MapOperationError)?;
|
||||
|
||||
let ns_map = ebpf.take_map("NEXT_STAGE").ok_or(EbpfError::MapNotFound)?;
|
||||
let mut next_stage = Array::<MapData, u32>::try_from(ns_map).map_err(EbpfError::MapOperationError)?;
|
||||
|
||||
// Load transmission (always present at STAGE_TRANSMISSION)
|
||||
Self::load_program(ebpf, &mut program_array, "transmission", STAGE_TRANSMISSION)?;
|
||||
|
||||
if stages.is_empty() {
|
||||
// Empty pipeline: entry → transmission
|
||||
next_stage
|
||||
.set(STAGE_ENTRY as u32, STAGE_TRANSMISSION, 0)
|
||||
.map_err(EbpfError::MapOperationError)?;
|
||||
return Ok(program_array);
|
||||
}
|
||||
|
||||
// Load each stage and assign a slot (starting from slot 1)
|
||||
let mut slots: Vec<(u32, u32)> = Vec::new(); // (stage_id, slot_index)
|
||||
let mut slots: Vec<(u32, u32)> = Vec::new();
|
||||
for (i, stage_name) in stages.iter().enumerate() {
|
||||
let (func_name, stage_id) = registry
|
||||
.get(stage_name.as_str())
|
||||
.ok_or(EbpfError::ProgramNotFound)?;
|
||||
let slot = (i + 1) as u32; // slots 1, 2, 3, ...
|
||||
let slot = (i + 1) as u32;
|
||||
Self::load_program(ebpf, &mut program_array, func_name, slot)?;
|
||||
slots.push((*stage_id, slot));
|
||||
}
|
||||
|
||||
// Wire NEXT_STAGE: entry → first slot
|
||||
next_stage
|
||||
.set(STAGE_ENTRY as u32, slots[0].1, 0)
|
||||
.map_err(EbpfError::MapOperationError)?;
|
||||
|
||||
// Wire each stage to the next
|
||||
for i in 0..slots.len() {
|
||||
let (stage_id, _) = slots[i];
|
||||
let next_slot = if i + 1 < slots.len() {
|
||||
@ -297,4 +303,11 @@ impl System {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_num_queues(ebpf: &mut Ebpf, num_queues: u32) -> Result<(), Error> {
|
||||
let map = ebpf.map_mut("NUM_QUEUES").ok_or(EbpfError::MapNotFound)?;
|
||||
let mut arr = Array::<_, u32>::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
arr.set(0, num_queues, 0).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,8 +32,15 @@ pub struct NetworkConfig {
|
||||
pub frame_size: u32,
|
||||
pub frame_count: u32,
|
||||
pub refresh_interval: u64,
|
||||
#[serde(default = "default_packet_buffer_size")]
|
||||
pub packet_buffer_size: usize,
|
||||
#[serde(default = "default_buffer_pool_capacity")]
|
||||
pub buffer_pool_capacity: usize,
|
||||
}
|
||||
|
||||
fn default_packet_buffer_size() -> usize { 2048 }
|
||||
fn default_buffer_pool_capacity() -> usize { 1024 }
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct InferenceConfig {
|
||||
pub deep_autoencoder_name: String,
|
||||
|
||||
@ -7,15 +7,6 @@ pub enum Direction {
|
||||
Egress,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
pub fn flip(self) -> Self {
|
||||
match self {
|
||||
Direction::Ingress => Direction::Egress,
|
||||
Direction::Egress => Direction::Ingress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Direction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
||||
@ -6,20 +6,20 @@ traceable! {
|
||||
#[error("Failed to initialize eBPF logger")]
|
||||
LoggerInitFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Ebpf program not found")]
|
||||
#[error("eBPF object not found")]
|
||||
EbpfNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to load XDP program")]
|
||||
#[error("XDP program not found")]
|
||||
ProgramNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to load XDP program")]
|
||||
#[error("Failed to get XDP program")]
|
||||
GetProgramFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to load XDP program")]
|
||||
LoadProgramFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to attach the XDP program")]
|
||||
#[error("Failed to attach XDP program")]
|
||||
AttachProgramFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to set umem")]
|
||||
@ -28,33 +28,33 @@ traceable! {
|
||||
#[error("Failed to set AF_XDP socket")]
|
||||
SocketSetFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to set AF_XDP")]
|
||||
#[error("Failed to configure AF_XDP")]
|
||||
AfXdpSetFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to wakeup TX")]
|
||||
WakeupTXFailed => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Map not found")]
|
||||
#[error("eBPF map not found")]
|
||||
MapNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[error("An error occurred during map operation")]
|
||||
#[error("eBPF map operation failed")]
|
||||
MapOperationError => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("The ip required for operation does not exist")]
|
||||
#[error("IP does not exist in map")]
|
||||
IpDoesNotExist => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Amount of rules has reached the upper limit")]
|
||||
#[error("Rule count has reached the upper limit")]
|
||||
RuleReachLimit => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Fill queue initialization failed: submitted fewer frames than expected")]
|
||||
#[error("Fill queue initialization failed")]
|
||||
FillQueueInitFailed => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Unknown error")]
|
||||
#[error("Unknown eBPF error")]
|
||||
UnknownError => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to spawn XSK thread")]
|
||||
@ -70,4 +70,3 @@ traceable! {
|
||||
TXQueueError => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,11 +19,16 @@ traceable! {
|
||||
#[error("Network interface '{interface}' not found")]
|
||||
NetworkInterfaceNotFound { interface: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Invalid GeoIP configuration")]
|
||||
InvalidGeoIPConfiguration => tracing::Level::ERROR,
|
||||
#[no_source]
|
||||
#[error("Failed to open GeoIP database '{path}': {reason}")]
|
||||
GeoIPDatabaseError { path: String, reason: String } => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to create traffic log file '{path}': {reason}")]
|
||||
TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Invalid DNS domain name: {reason}")]
|
||||
InvalidDnsName { reason: String } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,4 +20,4 @@ traceable! {
|
||||
#[error("Failed to parse inference configuration: {reason}")]
|
||||
ConfigParseFailed { reason: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ traceable! {
|
||||
#[error("Invalid configuration")]
|
||||
InvalidConfig => tracing::Level::ERROR,
|
||||
|
||||
#[error("Configuration not found")]
|
||||
#[error("Configuration file not found")]
|
||||
ConfigNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to terminate instance")]
|
||||
@ -20,14 +20,10 @@ traceable! {
|
||||
#[error("Failed to send shutdown signal")]
|
||||
ShutdownSignalFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Unexcepted thread panic")]
|
||||
#[error("Unexpected thread panic")]
|
||||
ThreadPanic => tracing::Level::ERROR,
|
||||
|
||||
#[error("Unexcepted error")]
|
||||
UnexpectError => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Unknown error")]
|
||||
UnknownError => tracing::Level::ERROR,
|
||||
#[error("Unexpected error")]
|
||||
UnexpectedError => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,6 @@ pub struct MemoryUsage {
|
||||
pub struct ConfiguredNetworkStats {
|
||||
pub ingress: Option<NetworkStats>,
|
||||
pub egress: Option<NetworkStats>,
|
||||
// pub management: Option<NetworkStats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
||||
@ -4,6 +4,6 @@ use tracing;
|
||||
loggable! {
|
||||
HttpLog {
|
||||
#[error("Health WebSocket lagged, skipped {skipped} messages")]
|
||||
WebSocketLaged { skipped: u64 } => tracing::Level::WARN,
|
||||
WebSocketLagged { skipped: u64 } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,70 +3,40 @@ use tracing;
|
||||
|
||||
loggable! {
|
||||
MLLog {
|
||||
#[error("Initializing Machine Learning with inference URL: {url}")]
|
||||
Initializing { url: String } => tracing::Level::INFO,
|
||||
|
||||
#[error("Continuing without Machine Learning detection")]
|
||||
Skiped => tracing::Level::WARN,
|
||||
|
||||
#[error("Machine Learning detection is disabled (no ml_inference_url configured)")]
|
||||
Disabled => tracing::Level::INFO,
|
||||
|
||||
#[error("Machine Learning detection starting")]
|
||||
Starting => tracing::Level::INFO,
|
||||
|
||||
#[error("Machine Learning detection ready")]
|
||||
Ready => tracing::Level::INFO,
|
||||
|
||||
#[error("Machine Learning channel disconnected")]
|
||||
ChannelDisconnected => tracing::Level::WARN,
|
||||
|
||||
#[error("Failed to forward packet: {error}")]
|
||||
ForwardPacketFailed { error: String } => tracing::Level::WARN,
|
||||
|
||||
#[error("Attach XDP program success")]
|
||||
AttachProgramSuccess => tracing::Level::INFO,
|
||||
|
||||
#[error("Queue initialization incomplete")]
|
||||
QueueInitIncomplete => tracing::Level::WARN,
|
||||
|
||||
#[error("Queue refill incomplete")]
|
||||
QueueRefillIncomplete => tracing::Level::WARN,
|
||||
|
||||
#[error("No frames submit to queue")]
|
||||
NoFrameSubmit => tracing::Level::WARN,
|
||||
|
||||
#[error("Queue pair {queue_id} started successfully")]
|
||||
QueuePairStarted { queue_id: u32 } => tracing::Level::INFO,
|
||||
|
||||
#[error("ML models loaded - {info}")]
|
||||
ModelsLoaded { info: String } => tracing::Level::INFO,
|
||||
|
||||
#[error("Inference configuration loaded: {features} features, {attacks} attack types")]
|
||||
ConfigLoaded { features: usize, attacks: usize } => tracing::Level::INFO,
|
||||
|
||||
#[error("Running inference on {size} flows")]
|
||||
RunningInference { size: usize } => tracing::Level::INFO,
|
||||
|
||||
#[error("Inference completed: {total_flows} flows ({anomaly} anomaly, {benign} benign) in {duration_ms}ms ({throughput:.1} flows/s)")]
|
||||
InferenceCompleted { total_flows: usize, anomaly: usize, benign: usize, duration_ms: u32, throughput: f32 } => tracing::Level::INFO,
|
||||
|
||||
#[error("Inference skipped: {reason}")]
|
||||
InferenceSkipped { reason: String } => tracing::Level::INFO,
|
||||
#[error("Inference returned fewer results: expected {size}, got {len}")]
|
||||
InferenceResults { size: usize, len: usize } => tracing::Level::WARN,
|
||||
|
||||
#[error("{model} inference failed: {error}")]
|
||||
InferenceFailed { model: String, error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Threat detected [{direction}]: {flow} -> {attack_type} (confidence: {confidence:.2}, ae_score: {ae_score:.4})")]
|
||||
ThreatDetected { direction: String, flow: String, attack_type: String, confidence: f32, ae_score: f32 } => tracing::Level::WARN,
|
||||
|
||||
#[error("Flow stats: total={total_flows}, qualified={flows_len}, min_packets={min_packets}, packet_counts: {counts}")]
|
||||
FlowStats { total_flows: usize, flows_len: usize, min_packets: usize, counts: String } => tracing::Level::INFO,
|
||||
|
||||
#[error("Running inference on {size} flows")]
|
||||
RunningInference { size: usize } => tracing::Level::INFO,
|
||||
|
||||
#[error("Inference returned fewer results: expected {size}, got {len}")]
|
||||
InferenceResults { size: usize, len: usize } => tracing::Level::INFO,
|
||||
|
||||
#[error("{model} inference failed: {error}")]
|
||||
InferenceFailed { model: String, error: String } => tracing::Level::INFO,
|
||||
FlowStats { total_flows: usize, flows_len: usize, min_packets: usize, counts: String } => tracing::Level::INFO,
|
||||
|
||||
#[error("Failed to parse packet (length: {len})")]
|
||||
ParsePacketFailed { len: usize } => tracing::Level::INFO,
|
||||
ParsePacketFailed { len: usize } => tracing::Level::WARN,
|
||||
|
||||
#[error("Failed to broadcast ML alert: {error}")]
|
||||
BroadcastAlertFailed { error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Traffic logger write error: {error}")]
|
||||
TrafficLogWriteError { error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Traffic logger channel disconnected")]
|
||||
TrafficLogChannelDisconnected => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,12 +19,12 @@ loggable! {
|
||||
TerminateComplete => tracing::Level::INFO,
|
||||
|
||||
#[error("Invalid configuration")]
|
||||
InvalidConfig => tracing::Level::INFO,
|
||||
InvalidConfig => tracing::Level::ERROR,
|
||||
|
||||
#[error("Configuration not found")]
|
||||
ConfigNotFound => tracing::Level::INFO,
|
||||
ConfigNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[error("Traffic logging mode enabled — writing packets to: {path}")]
|
||||
TrafficLoggingEnabled { path: String } => tracing::Level::INFO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tract_onnx::prelude::{Graph, SimplePlan, TypedFact, TypedOp};
|
||||
@ -35,15 +35,13 @@ pub struct FlowKey {
|
||||
|
||||
impl FlowKey {
|
||||
pub fn from_packet(packet: &UserPacket) -> Self {
|
||||
let (src_ip, ip_version) = Self::parse_ip_to_bytes(&packet.src_ip);
|
||||
let (dst_ip, _) = Self::parse_ip_to_bytes(&packet.dst_ip);
|
||||
Self {
|
||||
src_ip,
|
||||
dst_ip,
|
||||
src_ip: packet.src_ip,
|
||||
dst_ip: packet.dst_ip,
|
||||
src_port: packet.src_port,
|
||||
dst_port: packet.dst_port,
|
||||
protocol: packet.protocol,
|
||||
ip_version,
|
||||
ip_version: packet.ip_version,
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,21 +64,6 @@ impl FlowKey {
|
||||
self.ip_bytes_to_string(&self.dst_ip)
|
||||
}
|
||||
|
||||
fn parse_ip_to_bytes(ip_str: &str) -> ([u8; 16], u8) {
|
||||
if let Ok(addr) = ip_str.parse::<IpAddr>() {
|
||||
match addr {
|
||||
IpAddr::V4(v4) => {
|
||||
let mut buf = [0u8; 16];
|
||||
buf[..4].copy_from_slice(&v4.octets());
|
||||
(buf, 4)
|
||||
}
|
||||
IpAddr::V6(v6) => (v6.octets(), 6),
|
||||
}
|
||||
} else {
|
||||
([0u8; 16], 4)
|
||||
}
|
||||
}
|
||||
|
||||
fn ip_bytes_to_string(&self, bytes: &[u8; 16]) -> String {
|
||||
if self.ip_version == 6 {
|
||||
Ipv6Addr::from(*bytes).to_string()
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub struct UserPacket {
|
||||
#[allow(dead_code)]
|
||||
pub ip_version: u8,
|
||||
pub protocol: u8,
|
||||
pub tcp_flags: u8,
|
||||
pub src_ip: String,
|
||||
pub dst_ip: String,
|
||||
pub src_ip: [u8; 16],
|
||||
pub dst_ip: [u8; 16],
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub packet_length: u32,
|
||||
@ -14,3 +15,23 @@ pub struct UserPacket {
|
||||
pub timestamp_us: u64,
|
||||
pub is_forward: bool,
|
||||
}
|
||||
|
||||
impl UserPacket {
|
||||
/// Format source IP as a human-readable string.
|
||||
pub fn src_ip_string(&self) -> String {
|
||||
Self::ip_bytes_to_string(self.ip_version, &self.src_ip)
|
||||
}
|
||||
|
||||
/// Format destination IP as a human-readable string.
|
||||
pub fn dst_ip_string(&self) -> String {
|
||||
Self::ip_bytes_to_string(self.ip_version, &self.dst_ip)
|
||||
}
|
||||
|
||||
fn ip_bytes_to_string(ip_version: u8, bytes: &[u8; 16]) -> String {
|
||||
if ip_version == 6 {
|
||||
Ipv6Addr::from(*bytes).to_string()
|
||||
} else {
|
||||
Ipv4Addr::from([bytes[0], bytes[1], bytes[2], bytes[3]]).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_private_ip(ip: &IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => {
|
||||
|
||||
@ -30,30 +30,16 @@ fn parse_ipv4(packet_data: &[u8], timestamp_us: u64) -> Option<(UserPacket, usiz
|
||||
|
||||
let protocol_byte = ip_header[9];
|
||||
|
||||
let src_ip_raw = u32::from_be_bytes([ip_header[12], ip_header[13], ip_header[14], ip_header[15]]);
|
||||
let dst_ip_raw = u32::from_be_bytes([ip_header[16], ip_header[17], ip_header[18], ip_header[19]]);
|
||||
let mut src_ip = [0u8; 16];
|
||||
src_ip[..4].copy_from_slice(&ip_header[12..16]);
|
||||
let mut dst_ip = [0u8; 16];
|
||||
dst_ip[..4].copy_from_slice(&ip_header[16..20]);
|
||||
|
||||
let ihl = (ip_header[0] & 0x0F) as usize * 4;
|
||||
let total_len = u16::from_be_bytes([ip_header[2], ip_header[3]]) as u32;
|
||||
|
||||
if protocol_byte != 6 && protocol_byte != 17 {
|
||||
// Still track non-TCP/UDP packets (e.g. ICMP) for flow statistics
|
||||
let packet = UserPacket {
|
||||
ip_version: 4,
|
||||
protocol: protocol_byte,
|
||||
tcp_flags: 0,
|
||||
src_ip: format_ipv4(src_ip_raw),
|
||||
dst_ip: format_ipv4(dst_ip_raw),
|
||||
src_port: 0,
|
||||
dst_port: 0,
|
||||
packet_length: total_len,
|
||||
payload_length: 0,
|
||||
header_length: 0,
|
||||
tcp_window_size: 0,
|
||||
timestamp_us,
|
||||
is_forward: false,
|
||||
};
|
||||
return Some((packet, 14 + ihl));
|
||||
return None;
|
||||
}
|
||||
|
||||
if packet_data.len() < 14 + ihl + 4 {
|
||||
@ -87,8 +73,8 @@ fn parse_ipv4(packet_data: &[u8], timestamp_us: u64) -> Option<(UserPacket, usiz
|
||||
ip_version: 4,
|
||||
protocol: protocol_byte,
|
||||
tcp_flags,
|
||||
src_ip: format_ipv4(src_ip_raw),
|
||||
dst_ip: format_ipv4(dst_ip_raw),
|
||||
src_ip,
|
||||
dst_ip,
|
||||
src_port,
|
||||
dst_port,
|
||||
packet_length: total_len,
|
||||
@ -111,35 +97,17 @@ fn parse_ipv6(packet_data: &[u8], timestamp_us: u64) -> Option<(UserPacket, usiz
|
||||
|
||||
let protocol_byte = ip_header[6];
|
||||
|
||||
let mut source_ip_bytes = [0u8; 16];
|
||||
source_ip_bytes.copy_from_slice(&ip_header[8..24]);
|
||||
let src_ip_raw = u128::from_be_bytes(source_ip_bytes);
|
||||
let mut src_ip = [0u8; 16];
|
||||
src_ip.copy_from_slice(&ip_header[8..24]);
|
||||
|
||||
let mut dest_ip_bytes = [0u8; 16];
|
||||
dest_ip_bytes.copy_from_slice(&ip_header[24..40]);
|
||||
let dst_ip_raw = u128::from_be_bytes(dest_ip_bytes);
|
||||
let mut dst_ip = [0u8; 16];
|
||||
dst_ip.copy_from_slice(&ip_header[24..40]);
|
||||
|
||||
let payload_len = u16::from_be_bytes([ip_header[4], ip_header[5]]) as u32;
|
||||
let total_len = payload_len + 40;
|
||||
|
||||
if protocol_byte != 6 && protocol_byte != 17 {
|
||||
// Still track non-TCP/UDP packets (e.g. ICMPv6) for flow statistics
|
||||
let packet = UserPacket {
|
||||
ip_version: 6,
|
||||
protocol: protocol_byte,
|
||||
tcp_flags: 0,
|
||||
src_ip: format_ipv6(src_ip_raw),
|
||||
dst_ip: format_ipv6(dst_ip_raw),
|
||||
src_port: 0,
|
||||
dst_port: 0,
|
||||
packet_length: total_len,
|
||||
payload_length: 0,
|
||||
header_length: 0,
|
||||
tcp_window_size: 0,
|
||||
timestamp_us,
|
||||
is_forward: false,
|
||||
};
|
||||
return Some((packet, 14 + 40));
|
||||
return None;
|
||||
}
|
||||
|
||||
if packet_data.len() < 54 + 4 {
|
||||
@ -173,8 +141,8 @@ fn parse_ipv6(packet_data: &[u8], timestamp_us: u64) -> Option<(UserPacket, usiz
|
||||
ip_version: 6,
|
||||
protocol: protocol_byte,
|
||||
tcp_flags,
|
||||
src_ip: format_ipv6(src_ip_raw),
|
||||
dst_ip: format_ipv6(dst_ip_raw),
|
||||
src_ip,
|
||||
dst_ip,
|
||||
src_port,
|
||||
dst_port,
|
||||
packet_length: total_len,
|
||||
@ -188,12 +156,3 @@ fn parse_ipv6(packet_data: &[u8], timestamp_us: u64) -> Option<(UserPacket, usiz
|
||||
Some((packet, payload_start))
|
||||
}
|
||||
|
||||
pub fn format_ipv4(addr: u32) -> String {
|
||||
let bytes = addr.to_be_bytes();
|
||||
format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3],)
|
||||
}
|
||||
|
||||
pub fn format_ipv6(addr: u128) -> String {
|
||||
let bytes = addr.to_be_bytes();
|
||||
std::net::Ipv6Addr::from(bytes).to_string()
|
||||
}
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
use std::net::{SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use actix_web::{web, HttpResponse, Responder, Scope};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::core::ebpf::access_control::AccessControl;
|
||||
use crate::core::ebpf::geo_block::GeoBlock;
|
||||
use crate::model::direction::FlowDirection;
|
||||
use crate::model::list_type::ListType;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CountryCodesRequest {
|
||||
country_codes: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/acl")
|
||||
.route("/ipv4/{direction}/{list_type}", web::get().to(get_ipv4_list))
|
||||
@ -14,6 +21,9 @@ pub fn initialize() -> Scope {
|
||||
.route("/ipv6/{direction}/{list_type}", web::put().to(add_ipv6_list))
|
||||
.route("/ipv4/{direction}/{list_type}", web::delete().to(remove_ipv4_list))
|
||||
.route("/ipv6/{direction}/{list_type}", web::delete().to(remove_ipv6_list))
|
||||
.route("/geo/blocked", web::get().to(get_geo_blocked))
|
||||
.route("/geo/block", web::put().to(block_geo_countries))
|
||||
.route("/geo/unblock", web::delete().to(unblock_geo_countries))
|
||||
}
|
||||
|
||||
async fn get_ipv4_list(
|
||||
@ -85,3 +95,40 @@ async fn remove_ipv6_list(
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_geo_blocked(
|
||||
geo_block: web::Data<GeoBlock>,
|
||||
) -> impl Responder {
|
||||
let blocked = geo_block.get_blocked_countries();
|
||||
HttpResponse::Ok().json(serde_json::json!({"blocked_countries": blocked}))
|
||||
}
|
||||
|
||||
async fn block_geo_countries(
|
||||
body: web::Json<CountryCodesRequest>,
|
||||
geo_block: web::Data<GeoBlock>,
|
||||
) -> impl Responder {
|
||||
let codes = body.into_inner().country_codes;
|
||||
match geo_block.block_countries(&codes) {
|
||||
Ok(total_prefixes) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"blocked_countries": geo_block.get_blocked_countries(),
|
||||
"total_prefixes": total_prefixes,
|
||||
})),
|
||||
Err(e) => HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn unblock_geo_countries(
|
||||
body: web::Json<CountryCodesRequest>,
|
||||
geo_block: web::Data<GeoBlock>,
|
||||
) -> impl Responder {
|
||||
let codes = body.into_inner().country_codes;
|
||||
match geo_block.unblock_countries(&codes) {
|
||||
Ok(total_prefixes) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"blocked_countries": geo_block.get_blocked_countries(),
|
||||
"total_prefixes": total_prefixes,
|
||||
})),
|
||||
Err(e) => HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,80 @@
|
||||
use std::fmt;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use actix_web::{web, HttpResponse, Responder, Scope};
|
||||
use common::model::http_method::HttpMethod;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::core::ebpf::dns_filter::DnsFilter;
|
||||
use crate::core::ebpf::protocol_filter::ProtocolFilter;
|
||||
|
||||
/// Convert a fallible result into an Ok (200) or InternalServerError (500) response.
|
||||
fn ok_or_error<T, E: fmt::Display>(result: Result<T, E>) -> HttpResponse {
|
||||
match result {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/filter")
|
||||
.service(http_scope())
|
||||
.service(ssh_scope())
|
||||
.service(dns_scope())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DnsDomainsPayload {
|
||||
domains: Vec<String>,
|
||||
}
|
||||
|
||||
fn dns_scope() -> Scope {
|
||||
web::scope("/dns")
|
||||
.service(
|
||||
web::scope("/blacklist")
|
||||
.route("", web::get().to(get_dns_blacklist))
|
||||
.route("", web::put().to(add_dns_blacklist))
|
||||
.route("", web::delete().to(remove_dns_blacklist))
|
||||
)
|
||||
}
|
||||
|
||||
const MAX_DNS_DOMAINS_PER_REQUEST: usize = 1000;
|
||||
|
||||
async fn get_dns_blacklist(service: web::Data<DnsFilter>) -> impl Responder {
|
||||
HttpResponse::Ok().json(serde_json::json!({"domains": service.list_domains()}))
|
||||
}
|
||||
|
||||
async fn add_dns_blacklist(
|
||||
payload: web::Json<DnsDomainsPayload>,
|
||||
service: web::Data<DnsFilter>,
|
||||
) -> impl Responder {
|
||||
let domains = payload.into_inner().domains;
|
||||
if domains.len() > MAX_DNS_DOMAINS_PER_REQUEST {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(serde_json::json!({"error": format!("too many domains (max {})", MAX_DNS_DOMAINS_PER_REQUEST)}));
|
||||
}
|
||||
for domain in &domains {
|
||||
if let Err(e) = service.add_domain(domain) {
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().json(serde_json::json!({"added": domains.len()}))
|
||||
}
|
||||
|
||||
async fn remove_dns_blacklist(
|
||||
payload: web::Json<DnsDomainsPayload>,
|
||||
service: web::Data<DnsFilter>,
|
||||
) -> impl Responder {
|
||||
let domains = payload.into_inner().domains;
|
||||
for domain in &domains {
|
||||
if let Err(e) = service.remove_domain(domain) {
|
||||
return HttpResponse::InternalServerError()
|
||||
.json(serde_json::json!({"error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().json(serde_json::json!({"removed": domains.len()}))
|
||||
}
|
||||
|
||||
fn http_scope() -> Scope {
|
||||
@ -59,230 +125,119 @@ fn ssh_blacklist_scope() -> Scope {
|
||||
// --- HTTP service handlers ---
|
||||
|
||||
async fn get_ipv4_http_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv4_http_service().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv4_http_service().await)
|
||||
}
|
||||
|
||||
async fn get_ipv6_http_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv6_http_service().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv6_http_service().await)
|
||||
}
|
||||
|
||||
async fn add_ipv4_http_service(
|
||||
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
async fn add_ipv4_http_service(payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.add_ipv4_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_or_error(service.add_ipv4_http_service(addr, methods).await)
|
||||
}
|
||||
|
||||
async fn add_ipv6_http_service(
|
||||
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
async fn add_ipv6_http_service(payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.add_ipv6_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_or_error(service.add_ipv6_http_service(addr, methods).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv4_http_service(
|
||||
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
async fn remove_ipv4_http_service(payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.remove_ipv4_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_or_error(service.remove_ipv4_http_service(addr, methods).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv6_http_service(
|
||||
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
async fn remove_ipv6_http_service(payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.remove_ipv6_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_or_error(service.remove_ipv6_http_service(addr, methods).await)
|
||||
}
|
||||
|
||||
// --- SSH service handlers ---
|
||||
|
||||
async fn get_ipv4_ssh_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv4_ssh_service().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv4_ssh_service().await)
|
||||
}
|
||||
|
||||
async fn get_ipv6_ssh_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv6_ssh_service().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv6_ssh_service().await)
|
||||
}
|
||||
|
||||
async fn add_ipv4_ssh_service(
|
||||
ip_addr: web::Json<SocketAddrV4>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.add_ipv4_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn add_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv4_ssh_service(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn add_ipv6_ssh_service(
|
||||
ip_addr: web::Json<SocketAddrV6>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.add_ipv6_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn add_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv6_ssh_service(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv4_ssh_service(
|
||||
ip_addr: web::Json<SocketAddrV4>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.remove_ipv4_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn remove_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv4_ssh_service(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv6_ssh_service(
|
||||
ip_addr: web::Json<SocketAddrV6>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.remove_ipv6_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn remove_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv6_ssh_service(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
// --- SSH whitelist handlers ---
|
||||
|
||||
async fn is_ssh_white_list_enable(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let enabled = service.is_ssh_white_list_enable().await;
|
||||
HttpResponse::Ok().json(enabled)
|
||||
HttpResponse::Ok().json(service.is_ssh_white_list_enable().await)
|
||||
}
|
||||
|
||||
async fn enable_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
match service.enable_ssh_white_list().await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_or_error(service.enable_ssh_white_list().await)
|
||||
}
|
||||
|
||||
async fn disable_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
match service.disable_ssh_white_list().await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_or_error(service.disable_ssh_white_list().await)
|
||||
}
|
||||
|
||||
async fn get_ipv4_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv4_ssh_white_list().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv4_ssh_white_list().await)
|
||||
}
|
||||
|
||||
async fn get_ipv6_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv6_ssh_white_list().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv6_ssh_white_list().await)
|
||||
}
|
||||
|
||||
async fn add_ipv4_ssh_white_list(
|
||||
ip_addr: web::Json<Ipv4Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.add_ipv4_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn add_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv4_ssh_white_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn add_ipv6_ssh_white_list(
|
||||
ip_addr: web::Json<Ipv6Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.add_ipv6_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn add_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv6_ssh_white_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv4_ssh_white_list(
|
||||
ip_addr: web::Json<Ipv4Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.remove_ipv4_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn remove_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv4_ssh_white_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv6_ssh_white_list(
|
||||
ip_addr: web::Json<Ipv6Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.remove_ipv6_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn remove_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv6_ssh_white_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
// --- SSH blacklist handlers ---
|
||||
|
||||
async fn get_ipv4_ssh_black_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv4_ssh_black_list().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv4_ssh_black_list().await)
|
||||
}
|
||||
|
||||
async fn get_ipv6_ssh_black_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
let list = service.get_ipv6_ssh_black_list().await;
|
||||
HttpResponse::Ok().json(list)
|
||||
HttpResponse::Ok().json(service.get_ipv6_ssh_black_list().await)
|
||||
}
|
||||
|
||||
async fn add_ipv4_ssh_black_list(
|
||||
ip_addr: web::Json<Ipv4Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.add_ipv4_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn add_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv4_ssh_black_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn add_ipv6_ssh_black_list(
|
||||
ip_addr: web::Json<Ipv6Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.add_ipv6_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn add_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv6_ssh_black_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv4_ssh_black_list(
|
||||
ip_addr: web::Json<Ipv4Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.remove_ipv4_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn remove_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv4_ssh_black_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
async fn remove_ipv6_ssh_black_list(
|
||||
ip_addr: web::Json<Ipv6Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
) -> impl Responder {
|
||||
match service.remove_ipv6_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
async fn remove_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv6_ssh_black_list(ip_addr.into_inner()).await)
|
||||
}
|
||||
|
||||
@ -1,12 +1,27 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Scope};
|
||||
|
||||
use crate::core::ml::engine::Engine;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/ml")
|
||||
.route("/status", web::get().to(get_status))
|
||||
}
|
||||
|
||||
async fn get_status() -> impl Responder {
|
||||
async fn get_status(
|
||||
engine: web::Data<Engine>,
|
||||
) -> impl Responder {
|
||||
let trackers = engine.trackers();
|
||||
let num_trackers = trackers.len();
|
||||
let total_flows: usize = trackers.iter()
|
||||
.map(|t| t.lock().flow_count())
|
||||
.sum();
|
||||
let has_traffic_logger = engine.has_traffic_logger();
|
||||
|
||||
HttpResponse::Ok().json(serde_json::json!({
|
||||
"active": true
|
||||
"active": true,
|
||||
"mode": if has_traffic_logger { "traffic_logging" } else { "inference" },
|
||||
"num_trackers": num_trackers,
|
||||
"total_flows": total_flows,
|
||||
"inference_interval_secs": engine.inference_interval_secs(),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Scope};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use common::define::setting::*;
|
||||
|
||||
use crate::core::ebpf::rate_limit::RateLimitConfig;
|
||||
|
||||
@ -18,13 +19,15 @@ pub fn initialize() -> Scope {
|
||||
.route("/config", web::put().to(set_config))
|
||||
}
|
||||
|
||||
async fn get_config() -> impl Responder {
|
||||
async fn get_config(
|
||||
config: web::Data<RateLimitConfig>,
|
||||
) -> impl Responder {
|
||||
HttpResponse::Ok().json(RateLimitSettings {
|
||||
packet_rate: Some(common::model::rate_limit::DEFAULT_PACKET_RATE),
|
||||
syn_rate: Some(common::model::rate_limit::DEFAULT_SYN_RATE),
|
||||
udp_rate: Some(common::model::rate_limit::DEFAULT_UDP_RATE),
|
||||
dns_rate: Some(common::model::rate_limit::DEFAULT_DNS_RATE),
|
||||
window_ns: Some(common::model::rate_limit::DEFAULT_WINDOW_NS),
|
||||
packet_rate: Some(config.get_packet_rate().unwrap_or(DEFAULT_PACKET_RATE)),
|
||||
syn_rate: Some(config.get_syn_rate().unwrap_or(DEFAULT_SYN_RATE)),
|
||||
udp_rate: Some(config.get_udp_rate().unwrap_or(DEFAULT_UDP_RATE)),
|
||||
dns_rate: Some(config.get_dns_rate().unwrap_or(DEFAULT_DNS_RATE)),
|
||||
window_ns: Some(config.get_window_ns().unwrap_or(DEFAULT_WINDOW_NS)),
|
||||
})
|
||||
}
|
||||
|
||||
@ -33,10 +36,30 @@ async fn set_config(
|
||||
config: web::Data<RateLimitConfig>,
|
||||
) -> impl Responder {
|
||||
let s = settings.into_inner();
|
||||
if let Some(v) = s.packet_rate { let _ = config.set_packet_rate(v); }
|
||||
if let Some(v) = s.syn_rate { let _ = config.set_syn_rate(v); }
|
||||
if let Some(v) = s.udp_rate { let _ = config.set_udp_rate(v); }
|
||||
if let Some(v) = s.dns_rate { let _ = config.set_dns_rate(v); }
|
||||
if let Some(v) = s.window_ns { let _ = config.set_window_ns(v); }
|
||||
if let Some(v) = s.packet_rate {
|
||||
if let Err(e) = config.set_packet_rate(v) {
|
||||
return HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
if let Some(v) = s.syn_rate {
|
||||
if let Err(e) = config.set_syn_rate(v) {
|
||||
return HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
if let Some(v) = s.udp_rate {
|
||||
if let Err(e) = config.set_udp_rate(v) {
|
||||
return HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
if let Some(v) = s.dns_rate {
|
||||
if let Err(e) = config.set_dns_rate(v) {
|
||||
return HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
if let Some(v) = s.window_ns {
|
||||
if let Err(e) = config.set_window_ns(v) {
|
||||
return HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}));
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().json(serde_json::json!({"status": "ok"}))
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Scope};
|
||||
|
||||
use crate::core::ebpf::drop_monitor::DropMonitor;
|
||||
use crate::core::infrastructure::statistics::FlowStatistics;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
@ -7,6 +8,7 @@ pub fn initialize() -> Scope {
|
||||
.route("/flows", web::get().to(get_all_flows))
|
||||
.route("/flows/top/{n}", web::get().to(get_top_flows))
|
||||
.route("/summary", web::get().to(get_summary))
|
||||
.route("/drops", web::get().to(get_drop_stats))
|
||||
}
|
||||
|
||||
async fn get_all_flows(stats: web::Data<FlowStatistics>) -> impl Responder {
|
||||
@ -24,3 +26,7 @@ async fn get_top_flows(
|
||||
async fn get_summary(stats: web::Data<FlowStatistics>) -> impl Responder {
|
||||
HttpResponse::Ok().json(stats.get_summary())
|
||||
}
|
||||
|
||||
async fn get_drop_stats(monitor: web::Data<DropMonitor>) -> impl Responder {
|
||||
HttpResponse::Ok().json(monitor.get_counters())
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder, Scope};
|
||||
|
||||
use crate::core::ebpf::drop_monitor::DropMonitor;
|
||||
use crate::core::infrastructure::health::SystemHealth;
|
||||
use crate::core::infrastructure::statistics::FlowStatistics;
|
||||
use crate::core::ml::alert::MLAlert;
|
||||
use crate::web::websocket::{alert_websocket, flow_websocket, health_websocket};
|
||||
use crate::web::websocket::{alert_websocket, drop_websocket, flow_websocket, health_websocket};
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/ws")
|
||||
.route("/health", web::get().to(health_ws))
|
||||
.route("/alerts", web::get().to(alerts_ws))
|
||||
.route("/flows", web::get().to(flows_ws))
|
||||
.route("/drops", web::get().to(drops_ws))
|
||||
}
|
||||
|
||||
async fn health_ws(
|
||||
@ -44,3 +46,14 @@ async fn flows_ws(
|
||||
Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": format!("WebSocket error: {}", err)})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn drops_ws(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
monitor: web::Data<DropMonitor>,
|
||||
) -> impl Responder {
|
||||
match drop_websocket::websocket_drops(req, stream, monitor).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": format!("WebSocket error: {}", err)})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ async fn handle_alert_connection(
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLaged(skipped));
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
|
||||
90
net-guardia/src/web/websocket/drop_websocket.rs
Normal file
90
net-guardia/src/web/websocket/drop_websocket.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Result};
|
||||
use actix_ws::{handle, Message, MessageStream, Session};
|
||||
use futures_util::StreamExt;
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::core::ebpf::drop_monitor::{DropMonitor, DropEventMessage};
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::log::http::HttpLog;
|
||||
|
||||
pub async fn websocket_drops(
|
||||
req: HttpRequest,
|
||||
body: web::Payload,
|
||||
monitor: web::Data<DropMonitor>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
let broadcast_rx = monitor.subscribe();
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
handle_drop_connection(session, msg_stream, broadcast_rx).await;
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_drop_connection(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut broadcast_rx: broadcast::Receiver<DropEventMessage>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = broadcast_rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(event) => {
|
||||
if !send_drop_event(&mut session, &event).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_drop_event(session: &mut Session, event: &DropEventMessage) -> bool {
|
||||
match serde_json::to_string(event) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,10 @@ use std::time::Duration;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_ws::Message;
|
||||
use futures_util::StreamExt;
|
||||
use macros::log;
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::core::infrastructure::statistics::FlowStatistics;
|
||||
use crate::model::flow_stats::FlowSubscription;
|
||||
use crate::model::log::http::HttpLog;
|
||||
|
||||
/// Default subscription: all flows, no filter, 5 second interval
|
||||
fn default_subscription() -> FlowSubscription {
|
||||
@ -46,7 +44,6 @@ pub async fn flow_stats_ws(
|
||||
msg = msg_stream.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
// Client sends subscription query as JSON
|
||||
match serde_json::from_str::<FlowSubscription>(&text) {
|
||||
Ok(new_sub) => {
|
||||
let new_interval = new_sub.interval_secs.unwrap_or(5).max(1);
|
||||
@ -54,7 +51,6 @@ pub async fn flow_stats_ws(
|
||||
subscription.interval_secs = Some(new_interval);
|
||||
ticker = interval(Duration::from_secs(new_interval));
|
||||
|
||||
// Send immediate response with new filter
|
||||
let flows = stats.get_filtered_flows(&subscription);
|
||||
if let Ok(json) = serde_json::to_string(&flows) {
|
||||
if session.text(json).await.is_err() {
|
||||
|
||||
@ -46,7 +46,7 @@ async fn handle_health_connection(
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLaged(skipped));
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
@ -65,7 +65,6 @@ async fn handle_client_message(
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
// Text messages are intentionally ignored; no client commands are supported
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod alert_websocket;
|
||||
pub mod drop_websocket;
|
||||
pub mod flow_websocket;
|
||||
pub mod health_websocket;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user