mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-23 16:20:26 +09:00
feat: using research for academic
This commit is contained in:
commit
0d5fca3380
BIN
.github/images/accessControl.png
vendored
Normal file
BIN
.github/images/accessControl.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
.github/images/aiDetection.png
vendored
Normal file
BIN
.github/images/aiDetection.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
BIN
.github/images/dashboard.png
vendored
Normal file
BIN
.github/images/dashboard.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
.github/images/index.png
vendored
Normal file
BIN
.github/images/index.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
.github/images/map.png
vendored
Normal file
BIN
.github/images/map.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
BIN
.github/images/statistics.png
vendored
Normal file
BIN
.github/images/statistics.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
### https://raw.github.com/github/gitignore/master/Rust.gitignore
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
.idea
|
||||
logs
|
||||
TODO
|
||||
.log
|
||||
.txt
|
||||
net-guardia/static/web
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "net-guardia-frontend"]
|
||||
path = net-guardia-frontend
|
||||
url = https://github.com/ParrotXray/NetGuardia-frontend-academic-research.git
|
||||
3592
Cargo.lock
generated
Normal file
3592
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
Cargo.toml
Normal file
34
Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["net-guardia", "common", "macros", "ingress-ebpf", "egress-ebpf"]
|
||||
default-members = ["net-guardia", "common"]
|
||||
|
||||
[workspace.dependencies]
|
||||
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"] }
|
||||
xsk-rs = { version = "0.8.0", default-features = false }
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
#opt-level = 3
|
||||
#lto = true
|
||||
#strip = true
|
||||
#debug = false
|
||||
#overflow-checks = false
|
||||
|
||||
[profile.release.package.ingress-ebpf]
|
||||
debug = 2
|
||||
codegen-units = 1
|
||||
|
||||
[profile.release.package.egress-ebpf]
|
||||
debug = 2
|
||||
codegen-units = 1
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
||||
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>.
|
||||
60
README.md
Normal file
60
README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# NetGuardia
|
||||
|
||||
## Project Overview
|
||||
|
||||
**NetGuardia** is a high-performance network security solution that combines eBPF XDP technology with deep learning models to provide advanced network protection. The system operates as a standalone network appliance that can run on any Ubuntu-based system with compatible network hardware.
|
||||
|
||||
## Core Technologies
|
||||
|
||||
- **eBPF XDP Technology** - Provides high-performance packet processing directly at the data link layer
|
||||
- **Deep Learning Models** - Identifies and predicts potential network attacks with intelligent threat detection
|
||||
- **Hardware Integration** - Designed to work with Intel i350 T2 and similar enterprise-grade network interface cards
|
||||
|
||||
## Functional Modules
|
||||
|
||||
### Resource Overview
|
||||

|
||||
- Real-time control system occupancy rate
|
||||
|
||||
### Dashboard Overview
|
||||

|
||||
- Real-time network traffic monitoring and visualization
|
||||
- Recent traffic statistics and trend analysis
|
||||
|
||||
### Detailed Traffic Statistics
|
||||

|
||||
- Detailed traffic usage information per IP address
|
||||
|
||||
### Network Access Control
|
||||

|
||||
- IPv4/IPv6 whitelist and blacklist management
|
||||
- Precise port-level access control
|
||||
|
||||
[//]: # (### AI Attack Detection)
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # (- AI-based attack detection engine)
|
||||
|
||||
## System Features
|
||||
|
||||
- **High Performance** - Low-latency packet processing with minimal network performance impact
|
||||
- **User-Friendly** - Cross-platform web management interface with intuitive operation
|
||||
- **Reliability** - Hardware-accelerated processing ensures stable operation
|
||||
- **Scalability** - Modular design supports functional expansion
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Ubuntu-based operating system (Ubuntu 24.04 LTS or newer recommended)
|
||||
- Dual-port network interface card (Intel i350 T2 or compatible XDP-capable NIC)
|
||||
- Root/sudo access for eBPF program loading
|
||||
|
||||
## Hardware Compatibility
|
||||
NetGuardia is designed to work on any Ubuntu-based system meeting the following requirements:
|
||||
|
||||
- Network Interface: Any dual-port NIC supporting XDP native or offload mode (Intel i350 T2 recommended)
|
||||
- CPU: Multi-core processor recommended for optimal performance
|
||||
- Memory: 8GB RAM minimum, 16GB or more for high-traffic environments
|
||||
|
||||
The system is not limited to embedded platforms and can be deployed on standard server hardware, virtual machines, or dedicated appliances running Ubuntu.
|
||||
|
||||
18
common/Cargo.toml
Normal file
18
common/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
user = ["aya", "serde"]
|
||||
kernel = ["aya-ebpf"]
|
||||
|
||||
[dependencies]
|
||||
aya = { workspace = true, optional = true }
|
||||
aya-ebpf = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
network-types = { workspace = true }
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
4
common/src/define/mod.rs
Normal file
4
common/src/define/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod offset;
|
||||
pub mod other;
|
||||
pub mod program_array;
|
||||
pub mod setting;
|
||||
25
common/src/define/offset.rs
Normal file
25
common/src/define/offset.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use network_types::eth::EthHdr;
|
||||
use network_types::ip::{Ipv4Hdr, Ipv6Hdr};
|
||||
use network_types::tcp::TcpHdr;
|
||||
use network_types::udp::UdpHdr;
|
||||
|
||||
pub const ETHER_HEADER_START: usize = 0;
|
||||
pub const ETHER_HEADER_END: usize = ETHER_HEADER_START + size_of::<EthHdr>();
|
||||
|
||||
pub const IPV4_HEADER_START: usize = ETHER_HEADER_END;
|
||||
pub const IPV4_HEADER_END: usize = IPV4_HEADER_START + size_of::<Ipv4Hdr>();
|
||||
|
||||
pub const IPV6_HEADER_START: usize = ETHER_HEADER_END;
|
||||
pub const IPV6_HEADER_END: usize = IPV6_HEADER_START + size_of::<Ipv6Hdr>();
|
||||
|
||||
pub const IPV4_TCP_HEADER_START: usize = IPV4_HEADER_END;
|
||||
pub const IPV4_TCP_HEADER_END: usize = IPV4_TCP_HEADER_START + size_of::<TcpHdr>();
|
||||
|
||||
pub const IPV6_TCP_HEADER_START: usize = IPV6_HEADER_END;
|
||||
pub const IPV6_TCP_HEADER_END: usize = IPV6_TCP_HEADER_START + size_of::<TcpHdr>();
|
||||
|
||||
pub const IPV4_UDP_HEADER_START: usize = IPV4_HEADER_END;
|
||||
pub const IPV4_UDP_HEADER_END: usize = IPV4_UDP_HEADER_START + size_of::<UdpHdr>();
|
||||
|
||||
pub const IPV6_UDP_HEADER_START: usize = IPV6_HEADER_END;
|
||||
pub const IPV6_UDP_HEADER_END: usize = IPV6_UDP_HEADER_START + size_of::<UdpHdr>();
|
||||
1
common/src/define/other.rs
Normal file
1
common/src/define/other.rs
Normal file
@ -0,0 +1 @@
|
||||
pub const STANDARD_MTU: usize = 1500;
|
||||
11
common/src/define/program_array.rs
Normal file
11
common/src/define/program_array.rs
Normal file
@ -0,0 +1,11 @@
|
||||
pub mod ingress {
|
||||
pub const ACCESS_CONTROL: u32 = 0;
|
||||
pub const SERVICE: u32 = 1;
|
||||
pub const STATISTICS: u32 = 2;
|
||||
pub const TRANSMISSION: u32 = 3;
|
||||
}
|
||||
|
||||
pub mod egress {
|
||||
pub const STATISTICS: u32 = 0;
|
||||
pub const TRANSMISSION: u32 = 1;
|
||||
}
|
||||
4
common/src/define/setting.rs
Normal file
4
common/src/define/setting.rs
Normal file
@ -0,0 +1,4 @@
|
||||
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;
|
||||
1
common/src/ebpf/mod.rs
Normal file
1
common/src/ebpf/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod parsing;
|
||||
147
common/src/ebpf/parsing.rs
Normal file
147
common/src/ebpf/parsing.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use aya_ebpf::helpers::bpf_ktime_get_ns;
|
||||
use network_types::eth::{EthHdr, EtherType};
|
||||
use network_types::ip::{IpProto, Ipv4Hdr, Ipv6Hdr};
|
||||
use network_types::tcp::TcpHdr;
|
||||
use network_types::udp::UdpHdr;
|
||||
|
||||
use crate::define::offset::*;
|
||||
use crate::model::event::{Event, IPv4Event, IPv6Event};
|
||||
|
||||
pub fn parse_packet(start: usize, end: usize, target: *mut Event) -> Result<(), ()> {
|
||||
unsafe {
|
||||
if start + ETHER_HEADER_END > end {
|
||||
return Err(());
|
||||
}
|
||||
let eth = &*((start + ETHER_HEADER_START) as *const EthHdr);
|
||||
let ether_type = eth.ether_type().map_err(|_| ())?;
|
||||
match ether_type {
|
||||
EtherType::Ipv4 => parse_ipv4_packet(start, end, target),
|
||||
EtherType::Ipv6 => parse_ipv6_packet(start, end, target),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_ipv4_packet(start: usize, end: usize, target: *mut Event) -> Result<(), ()> {
|
||||
unsafe {
|
||||
if start + IPV4_HEADER_END > end {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let ipv4 = &*((start + IPV4_HEADER_START) as *const Ipv4Hdr);
|
||||
|
||||
let (source_port, destination_port) = match ipv4.proto {
|
||||
IpProto::Tcp => parse_tcp_port(start, end, IPV4_TCP_HEADER_START, IPV4_TCP_HEADER_END)?,
|
||||
IpProto::Udp => parse_udp_port(start, end, IPV4_UDP_HEADER_START, IPV4_UDP_HEADER_END)?,
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
*(target as *mut u32) = 0;
|
||||
|
||||
let ipv4_data_ptr = (target as *mut u8).add(16);
|
||||
|
||||
core::ptr::write(ipv4_data_ptr as *mut IpProto, ipv4.proto);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
ipv4.src_addr.as_ptr(),
|
||||
ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, src_ip)),
|
||||
4,
|
||||
);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
ipv4.dst_addr.as_ptr(),
|
||||
ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, dst_ip)),
|
||||
4,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, src_port)) as *mut u16,
|
||||
source_port,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, dst_port)) as *mut u16,
|
||||
destination_port,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, packet_length)) as *mut u32,
|
||||
(end - start) as u32,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, timestamp_us)) as *mut u64,
|
||||
bpf_ktime_get_ns(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_ipv6_packet(start: usize, end: usize, target: *mut Event) -> Result<(), ()> {
|
||||
unsafe {
|
||||
if start + IPV6_HEADER_END > end {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let ipv6 = &*((start + IPV6_HEADER_START) as *const Ipv6Hdr);
|
||||
|
||||
let (source_port, destination_port) = match ipv6.next_hdr {
|
||||
IpProto::Tcp => parse_tcp_port(start, end, IPV6_TCP_HEADER_START, IPV6_TCP_HEADER_END)?,
|
||||
IpProto::Udp => parse_udp_port(start, end, IPV6_UDP_HEADER_START, IPV6_UDP_HEADER_END)?,
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
*(target as *mut u32) = 1;
|
||||
|
||||
let ipv6_data_ptr = (target as *mut u8).add(16);
|
||||
|
||||
core::ptr::write(ipv6_data_ptr as *mut IpProto, ipv6.next_hdr);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
ipv6.src_addr.as_ptr(),
|
||||
ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, src_ip)),
|
||||
16,
|
||||
);
|
||||
core::ptr::copy_nonoverlapping(
|
||||
ipv6.dst_addr.as_ptr(),
|
||||
ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, dst_ip)),
|
||||
16,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, src_port)) as *mut u16,
|
||||
source_port,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, dst_port)) as *mut u16,
|
||||
destination_port,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, packet_length)) as *mut u32,
|
||||
(end - start) as u32,
|
||||
);
|
||||
core::ptr::write(
|
||||
ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, timestamp_us)) as *mut u64,
|
||||
bpf_ktime_get_ns(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_tcp_port(start: usize, end: usize, tcp_start: usize, tcp_end: usize) -> Result<(u16, u16), ()> {
|
||||
unsafe {
|
||||
if start + tcp_end > end {
|
||||
return Err(());
|
||||
}
|
||||
let tcp = &*((start + tcp_start) as *const TcpHdr);
|
||||
Ok((u16::from_be_bytes(tcp.source), u16::from_be_bytes(tcp.dest)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn parse_udp_port(start: usize, end: usize, udp_start: usize, udp_end: usize) -> Result<(u16, u16), ()> {
|
||||
unsafe {
|
||||
if start + udp_end > end {
|
||||
return Err(());
|
||||
}
|
||||
let udp = &*((start + udp_start) as *const UdpHdr);
|
||||
Ok((udp.src_port(), udp.dst_port()))
|
||||
}
|
||||
}
|
||||
9
common/src/lib.rs
Normal file
9
common/src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
||||
#![no_std]
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
extern crate std;
|
||||
|
||||
pub mod define;
|
||||
#[cfg(feature = "kernel")]
|
||||
pub mod ebpf;
|
||||
pub mod model;
|
||||
197
common/src/model/event.rs
Normal file
197
common/src/model/event.rs
Normal file
@ -0,0 +1,197 @@
|
||||
use network_types::ip::IpProto;
|
||||
|
||||
use crate::model::ip_address::{AddrPortV4, AddrPortV6};
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
IPv4(IPv4Event),
|
||||
IPv6(IPv6Event),
|
||||
}
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Clone)]
|
||||
pub enum RawIp {
|
||||
V4(u32),
|
||||
V6(u128),
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn timestamp_us(&self) -> u64 {
|
||||
match self {
|
||||
Event::IPv4(e) => e.timestamp_us,
|
||||
Event::IPv6(e) => e.timestamp_us,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn packet_length(&self) -> u32 {
|
||||
match self {
|
||||
Event::IPv4(e) => e.packet_length,
|
||||
Event::IPv6(e) => e.packet_length,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header_length(&self) -> u16 {
|
||||
match self {
|
||||
Event::IPv4(e) => e.header_length,
|
||||
Event::IPv6(e) => e.header_length,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn payload_length(&self) -> u32 {
|
||||
match self {
|
||||
Event::IPv4(e) => e.payload_length,
|
||||
Event::IPv6(e) => e.payload_length,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tcp_flags(&self) -> &TcpFlags {
|
||||
match self {
|
||||
Event::IPv4(e) => &e.tcp_flags,
|
||||
Event::IPv6(e) => &e.tcp_flags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tcp_window_size(&self) -> u16 {
|
||||
match self {
|
||||
Event::IPv4(e) => e.tcp_window_size,
|
||||
Event::IPv6(e) => e.tcp_window_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_forward(&self) -> bool {
|
||||
match self {
|
||||
Event::IPv4(e) => e.is_forward,
|
||||
Event::IPv6(e) => e.is_forward,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn protocol(&self) -> &IpProto {
|
||||
match self {
|
||||
Event::IPv4(e) => &e.protocol,
|
||||
Event::IPv6(e) => &e.protocol,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn src_ip(&self) -> RawIp {
|
||||
match self {
|
||||
Event::IPv4(e) => RawIp::V4(e.src_ip),
|
||||
Event::IPv6(e) => RawIp::V6(e.src_ip),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dst_ip(&self) -> RawIp {
|
||||
match self {
|
||||
Event::IPv4(e) => RawIp::V4(e.dst_ip),
|
||||
Event::IPv6(e) => RawIp::V6(e.dst_ip),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn src_port(&self) -> u16 {
|
||||
match self {
|
||||
Event::IPv4(e) => e.src_port,
|
||||
Event::IPv6(e) => e.src_port,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dst_port(&self) -> u16 {
|
||||
match self {
|
||||
Event::IPv4(e) => e.dst_port,
|
||||
Event::IPv6(e) => e.dst_port,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_is_forward(&mut self, value: bool) {
|
||||
match self {
|
||||
Event::IPv4(e) => e.is_forward = value,
|
||||
Event::IPv6(e) => e.is_forward = value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IPv4Event {
|
||||
pub protocol: IpProto,
|
||||
pub src_ip: u32,
|
||||
pub dst_ip: u32,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub packet_length: u32,
|
||||
pub payload_length: u32,
|
||||
pub header_length: u16,
|
||||
pub timestamp_us: u64,
|
||||
pub tcp_flags: TcpFlags,
|
||||
pub tcp_window_size: u16,
|
||||
pub is_forward: bool,
|
||||
}
|
||||
|
||||
impl IPv4Event {
|
||||
#[inline(always)]
|
||||
pub fn source_addr(&self) -> AddrPortV4 {
|
||||
AddrPortV4::new(self.src_ip, self.src_port)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn destination_addr(&self) -> AddrPortV4 {
|
||||
AddrPortV4::new(self.dst_ip, self.dst_port)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Clone)]
|
||||
pub struct IPv6Event {
|
||||
pub protocol: IpProto,
|
||||
pub src_ip: u128,
|
||||
pub dst_ip: u128,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub packet_length: u32,
|
||||
pub payload_length: u32,
|
||||
pub header_length: u16,
|
||||
pub timestamp_us: u64,
|
||||
pub tcp_flags: TcpFlags,
|
||||
pub tcp_window_size: u16,
|
||||
pub is_forward: bool,
|
||||
}
|
||||
|
||||
impl IPv6Event {
|
||||
#[inline(always)]
|
||||
pub fn source_addr(&self) -> AddrPortV6 {
|
||||
AddrPortV6::new(self.src_ip, self.src_port)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn destination_addr(&self) -> AddrPortV6 {
|
||||
AddrPortV6::new(self.dst_ip, self.dst_port)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TcpFlags {
|
||||
pub fin: bool,
|
||||
pub syn: bool,
|
||||
pub rst: bool,
|
||||
pub psh: bool,
|
||||
pub ack: bool,
|
||||
pub urg: bool,
|
||||
pub ece: bool,
|
||||
pub cwr: bool,
|
||||
}
|
||||
|
||||
impl TcpFlags {
|
||||
pub fn from_byte(flags: u8) -> Self {
|
||||
Self {
|
||||
fin: (flags & 0x01) != 0,
|
||||
syn: (flags & 0x02) != 0,
|
||||
rst: (flags & 0x04) != 0,
|
||||
psh: (flags & 0x08) != 0,
|
||||
ack: (flags & 0x10) != 0,
|
||||
urg: (flags & 0x20) != 0,
|
||||
ece: (flags & 0x40) != 0,
|
||||
cwr: (flags & 0x80) != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
26
common/src/model/flow_stats.rs
Normal file
26
common/src/model/flow_stats.rs
Normal file
@ -0,0 +1,26 @@
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
#[cfg(feature = "user")]
|
||||
use serde::Serialize;
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Clone, Copy)]
|
||||
#[cfg_attr(feature = "user", derive(Serialize, Debug))]
|
||||
pub struct FlowStats {
|
||||
pub bytes: u64,
|
||||
pub packets: u64,
|
||||
pub last_seen: u64,
|
||||
}
|
||||
|
||||
impl FlowStats {
|
||||
pub fn new(bytes: u64, packets: u64, last_seen: u64) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
packets,
|
||||
last_seen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for FlowStats {}
|
||||
56
common/src/model/http_method.rs
Normal file
56
common/src/model/http_method.rs
Normal file
@ -0,0 +1,56 @@
|
||||
#[cfg(all(feature = "user"))]
|
||||
use std::vec::Vec;
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type HttpMethodBitmap = u16;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[cfg_attr(feature = "user", derive(Serialize, Deserialize, Debug, Eq, PartialEq))]
|
||||
pub enum HttpMethod {
|
||||
GET = 0b0000_0000_0000_0001,
|
||||
POST = 0b0000_0000_0000_0010,
|
||||
PUT = 0b0000_0000_0000_0100,
|
||||
DELETE = 0b0000_0000_0000_1000,
|
||||
HEAD = 0b0000_0000_0001_0000,
|
||||
OPTIONS = 0b0000_0000_0010_0000,
|
||||
PATCH = 0b0000_0000_0100_0000,
|
||||
TRACE = 0b0000_0000_1000_0000,
|
||||
CONNECT = 0b0000_0001_0000_0000,
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
impl HttpMethod {
|
||||
pub fn convert_from_bitmap(http_method_bitmap: HttpMethodBitmap) -> Vec<HttpMethod> {
|
||||
let value = http_method_bitmap as u16;
|
||||
let mut http_methods = Vec::new();
|
||||
|
||||
let all_methods = [
|
||||
HttpMethod::GET,
|
||||
HttpMethod::POST,
|
||||
HttpMethod::PUT,
|
||||
HttpMethod::DELETE,
|
||||
HttpMethod::HEAD,
|
||||
HttpMethod::OPTIONS,
|
||||
HttpMethod::PATCH,
|
||||
HttpMethod::TRACE,
|
||||
HttpMethod::CONNECT,
|
||||
];
|
||||
|
||||
for method in all_methods {
|
||||
if value & (method as u16) != 0 {
|
||||
http_methods.push(method);
|
||||
}
|
||||
}
|
||||
http_methods
|
||||
}
|
||||
|
||||
pub fn convert_to_bitmap(http_methods: Vec<HttpMethod>) -> HttpMethodBitmap {
|
||||
let mut ebpf_http_method = 0_u16;
|
||||
for http_method in http_methods {
|
||||
ebpf_http_method |= http_method as u16;
|
||||
}
|
||||
ebpf_http_method
|
||||
}
|
||||
}
|
||||
78
common/src/model/ip_address.rs
Normal file
78
common/src/model/ip_address.rs
Normal file
@ -0,0 +1,78 @@
|
||||
#[cfg(feature = "user")]
|
||||
use aya::Pod;
|
||||
|
||||
pub type IPv4 = u32;
|
||||
pub type IPv6 = u128;
|
||||
pub type Port = u16;
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AddrPortV4([u8; 8]);
|
||||
|
||||
impl AddrPortV4 {
|
||||
#[inline(always)]
|
||||
pub fn new(ip: u32, port: u16) -> Self {
|
||||
let mut key = [0u8; 8];
|
||||
key[0..4].copy_from_slice(&ip.to_ne_bytes());
|
||||
key[4..6].copy_from_slice(&port.to_ne_bytes());
|
||||
Self(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_bytes(&self) -> &[u8; 8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ip(&self) -> IPv4 {
|
||||
let mut ip_bytes = [0u8; 4];
|
||||
ip_bytes.copy_from_slice(&self.0[0..4]);
|
||||
u32::from_ne_bytes(ip_bytes)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn port(&self) -> Port {
|
||||
let mut port_bytes = [0u8; 2];
|
||||
port_bytes.copy_from_slice(&self.0[4..6]);
|
||||
u16::from_ne_bytes(port_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for AddrPortV4 {}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AddrPortV6([u8; 32]);
|
||||
|
||||
impl AddrPortV6 {
|
||||
#[inline(always)]
|
||||
pub fn new(ip: u128, port: u16) -> Self {
|
||||
let mut key = [0u8; 32];
|
||||
key[0..16].copy_from_slice(&ip.to_ne_bytes());
|
||||
key[16..18].copy_from_slice(&port.to_ne_bytes());
|
||||
Self(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ip(&self) -> IPv6 {
|
||||
let mut ip_bytes = [0u8; 16];
|
||||
ip_bytes.copy_from_slice(&self.0[0..16]);
|
||||
u128::from_ne_bytes(ip_bytes)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn port(&self) -> Port {
|
||||
let mut port_bytes = [0u8; 2];
|
||||
port_bytes.copy_from_slice(&self.0[16..18]);
|
||||
u16::from_ne_bytes(port_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
unsafe impl Pod for AddrPortV6 {}
|
||||
7
common/src/model/mod.rs
Normal file
7
common/src/model/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod event;
|
||||
pub mod flow_stats;
|
||||
pub mod http_method;
|
||||
pub mod ip_address;
|
||||
pub mod packet;
|
||||
pub mod placeholder;
|
||||
pub mod pseudo_header;
|
||||
5
common/src/model/packet.rs
Normal file
5
common/src/model/packet.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use crate::define::other::STANDARD_MTU;
|
||||
use crate::model::event::Event;
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct Packet(pub [u8; size_of::<Event>() + STANDARD_MTU]);
|
||||
1
common/src/model/placeholder.rs
Normal file
1
common/src/model/placeholder.rs
Normal file
@ -0,0 +1 @@
|
||||
pub type PlaceHolder = u8;
|
||||
19
common/src/model/pseudo_header.rs
Normal file
19
common/src/model/pseudo_header.rs
Normal file
@ -0,0 +1,19 @@
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IPv4PseudoHeader {
|
||||
pub source_ip: u32,
|
||||
pub destination_ip: u32,
|
||||
pub zeros: u8,
|
||||
pub protocol: u8,
|
||||
pub length: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IPv6PseudoHeader {
|
||||
pub source_ip: u128,
|
||||
pub destination_ip: u128,
|
||||
pub length: u16,
|
||||
pub zeros: u8,
|
||||
pub next_header: u8,
|
||||
}
|
||||
26
config.toml
Normal file
26
config.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[Config]
|
||||
ingress_ifname = "enp4s0f1" # Ingress NIC Name
|
||||
egress_ifname = "enp4s0f0" # Egress NIC Name
|
||||
geoip_db_name = "GeoLite2-City.mmdb"
|
||||
deep_autoencoder_name = "deep_autoencoder.onnx"
|
||||
classifier_name = "classifier.onnx"
|
||||
models_config_name = "inference_config.json"
|
||||
combined_queue_count = 8 # NIC Combined Queue Count (ethtool -l <NIC>)
|
||||
channel_size = 4096
|
||||
fill_queue_size = 4096 # Umem Used (Should not modify)
|
||||
comp_queue_size = 4096 # Umem Used (Should not modify)
|
||||
tx_queue_size = 4096 # Umem Used (Should not modify)
|
||||
rx_queue_size = 4096 # Umem Used (Should not modify)
|
||||
frame_size = 4096 # Umem Used (Should not modify)
|
||||
frame_count = 4096 # Umem Used (Should not modify)
|
||||
http_server_bind_port = 8080 # Http Server Listen Port
|
||||
refresh_interval = 5 # Statistics Refresh Time
|
||||
|
||||
max_concurrent_flows = 10000 # max_flows: track up to 10000 concurrent flows
|
||||
min_packets_for_inference = 5 # min_packets: minimum 10 packets per flow for inference
|
||||
inference_interval_secs = 5 # interval_secs: run inference every 5 seconds
|
||||
aggregator_window_secs = 30
|
||||
inference_batch_size = 200
|
||||
|
||||
traffic_logging_mode = true # When true, disables ML inference and records all ingress/egress packets to CSV
|
||||
traffic_log_csv_path = "traffic_log.csv" # Output CSV file path for traffic logging mode
|
||||
5
egress-ebpf/.cargo/config.toml
Normal file
5
egress-ebpf/.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[build]
|
||||
target = ["bpfeb-unknown-none", "bpfel-unknown-none"]
|
||||
|
||||
[unstable]
|
||||
build-std = ["core"]
|
||||
20
egress-ebpf/Cargo.toml
Normal file
20
egress-ebpf/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "egress-ebpf"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common", features = ["kernel"] }
|
||||
|
||||
aya-ebpf = { workspace = true }
|
||||
aya-log-ebpf = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
which = "8.0.0"
|
||||
|
||||
[[bin]]
|
||||
name = "net-guardia-egress"
|
||||
path = "src/main.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
bench = false
|
||||
6
egress-ebpf/build.rs
Normal file
6
egress-ebpf/build.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use which::which;
|
||||
|
||||
fn main() {
|
||||
let bpf_linker = which("bpf-linker").unwrap();
|
||||
println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap());
|
||||
}
|
||||
3
egress-ebpf/rust-toolchain.toml
Normal file
3
egress-ebpf/rust-toolchain.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src"]
|
||||
1
egress-ebpf/src/action/mod.rs
Normal file
1
egress-ebpf/src/action/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod statistics;
|
||||
93
egress-ebpf/src/action/statistics.rs
Normal file
93
egress-ebpf/src/action/statistics.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::LruHashMap;
|
||||
use common::define::setting::MAX_STATS;
|
||||
use common::model::event::{IPv4Event, IPv6Event};
|
||||
use common::model::flow_stats::FlowStats;
|
||||
use common::model::ip_address::{AddrPortV4, AddrPortV6};
|
||||
|
||||
#[map]
|
||||
static IPV4_EGRESS_SRC_1MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_EGRESS_SRC_10MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_EGRESS_SRC_1HOUR: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_EGRESS_SRC_1MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_EGRESS_SRC_10MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_EGRESS_SRC_1HOUR: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_EGRESS_DST_1MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_EGRESS_DST_10MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_EGRESS_DST_1HOUR: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_EGRESS_DST_1MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_EGRESS_DST_10MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_EGRESS_DST_1HOUR: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
|
||||
pub fn ipv4_update_stats(event: &IPv4Event) {
|
||||
unsafe {
|
||||
let source = event.source_addr();
|
||||
let destination = event.destination_addr();
|
||||
ipv4_update_flow_stats(&IPV4_EGRESS_SRC_1MIN, &source, event);
|
||||
ipv4_update_flow_stats(&IPV4_EGRESS_SRC_10MIN, &source, event);
|
||||
ipv4_update_flow_stats(&IPV4_EGRESS_SRC_1HOUR, &source, event);
|
||||
ipv4_update_flow_stats(&IPV4_EGRESS_DST_1MIN, &destination, event);
|
||||
ipv4_update_flow_stats(&IPV4_EGRESS_DST_10MIN, &destination, event);
|
||||
ipv4_update_flow_stats(&IPV4_EGRESS_DST_1HOUR, &destination, event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipv6_update_stats(event: &IPv6Event) {
|
||||
unsafe {
|
||||
let source = event.source_addr();
|
||||
let destination = event.destination_addr();
|
||||
ipv6_update_flow_status(&IPV6_EGRESS_SRC_1MIN, &source, event);
|
||||
ipv6_update_flow_status(&IPV6_EGRESS_SRC_10MIN, &source, event);
|
||||
ipv6_update_flow_status(&IPV6_EGRESS_SRC_1HOUR, &source, event);
|
||||
ipv6_update_flow_status(&IPV6_EGRESS_DST_1MIN, &destination, event);
|
||||
ipv6_update_flow_status(&IPV6_EGRESS_DST_10MIN, &destination, event);
|
||||
ipv6_update_flow_status(&IPV6_EGRESS_DST_1HOUR, &destination, event);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn ipv4_update_flow_stats(map: &LruHashMap<AddrPortV4, FlowStats>, key: &AddrPortV4, event: &IPv4Event) {
|
||||
unsafe {
|
||||
if let Some(status) = map.get_ptr_mut(key) {
|
||||
(*status).bytes += event.packet_length as u64;
|
||||
(*status).packets += 1;
|
||||
(*status).last_seen = event.timestamp_us;
|
||||
} else {
|
||||
let new_stats = FlowStats {
|
||||
bytes: event.packet_length as u64,
|
||||
packets: 1,
|
||||
last_seen: event.timestamp_us,
|
||||
};
|
||||
let _ = map.insert(key, &new_stats, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn ipv6_update_flow_status(map: &LruHashMap<AddrPortV6, FlowStats>, key: &AddrPortV6, event: &IPv6Event) {
|
||||
unsafe {
|
||||
if let Some(status) = map.get_ptr_mut(key) {
|
||||
(*status).bytes += event.packet_length as u64;
|
||||
(*status).packets += 1;
|
||||
(*status).last_seen = event.timestamp_us;
|
||||
} else {
|
||||
let new_stats = FlowStats {
|
||||
bytes: event.packet_length as u64,
|
||||
packets: 1,
|
||||
last_seen: event.timestamp_us,
|
||||
};
|
||||
let _ = map.insert(key, &new_stats, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
egress-ebpf/src/lib.rs
Normal file
1
egress-ebpf/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
#![no_std]
|
||||
79
egress-ebpf/src/main.rs
Normal file
79
egress-ebpf/src/main.rs
Normal file
@ -0,0 +1,79 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
mod action;
|
||||
|
||||
use action::statistics;
|
||||
use aya_ebpf::bindings::xdp_action;
|
||||
use aya_ebpf::macros::{map, xdp};
|
||||
use aya_ebpf::maps::{PerCpuArray, ProgramArray, XskMap};
|
||||
use aya_ebpf::programs::XdpContext;
|
||||
#[allow(unused_imports)]
|
||||
use aya_log_ebpf::info;
|
||||
use common::define::program_array::egress::*;
|
||||
use common::{ebpf::parsing, model::event::Event};
|
||||
|
||||
#[map]
|
||||
static PROGRAM_ARRAY: ProgramArray = ProgramArray::with_max_entries(8, 0);
|
||||
#[map]
|
||||
static PARSED_PACKET: PerCpuArray<Event> = PerCpuArray::with_max_entries(1, 0);
|
||||
#[map]
|
||||
static EGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0);
|
||||
|
||||
#[xdp]
|
||||
pub fn net_guardia(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
let _ = packet_intake(ctx);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn packet_intake(ctx: XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let start = ctx.data();
|
||||
let end = ctx.data_end();
|
||||
let ptr = PARSED_PACKET.get_ptr_mut(0).ok_or(())?;
|
||||
parsing::parse_packet(start, end, ptr)?;
|
||||
let _ = PROGRAM_ARRAY.tail_call(&ctx, STATISTICS);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn statistics(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
let _ = try_statistics(&ctx);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn try_statistics(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let parsed_packet = &*ptr;
|
||||
match parsed_packet {
|
||||
Event::IPv4(event) => {
|
||||
statistics::ipv4_update_stats(event);
|
||||
}
|
||||
Event::IPv6(event) => {
|
||||
statistics::ipv6_update_stats(event);
|
||||
}
|
||||
}
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, TRANSMISSION);
|
||||
Ok(xdp_action::XDP_PASS)
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn transmission(ctx: XdpContext) -> u32 {
|
||||
let queue_id = unsafe { (*ctx.ctx).rx_queue_index };
|
||||
match EGRESS_XSKS_MAP.redirect(queue_id, 0) {
|
||||
Ok(action) => action,
|
||||
Err(_) => xdp_action::XDP_PASS,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
unsafe { core::hint::unreachable_unchecked() }
|
||||
}
|
||||
5
ingress-ebpf/.cargo/config.toml
Normal file
5
ingress-ebpf/.cargo/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[build]
|
||||
target = ["bpfeb-unknown-none", "bpfel-unknown-none"]
|
||||
|
||||
[unstable]
|
||||
build-std = ["core"]
|
||||
21
ingress-ebpf/Cargo.toml
Normal file
21
ingress-ebpf/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "ingress-ebpf"
|
||||
version = "0.1.0"
|
||||
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"
|
||||
|
||||
[[bin]]
|
||||
name = "net-guardia-ingress"
|
||||
path = "src/main.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
bench = false
|
||||
6
ingress-ebpf/build.rs
Normal file
6
ingress-ebpf/build.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use which::which;
|
||||
|
||||
fn main() {
|
||||
let bpf_linker = which("bpf-linker").unwrap();
|
||||
println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap());
|
||||
}
|
||||
3
ingress-ebpf/rust-toolchain.toml
Normal file
3
ingress-ebpf/rust-toolchain.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src"]
|
||||
102
ingress-ebpf/src/action/access_control.rs
Normal file
102
ingress-ebpf/src/action/access_control.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::HashMap;
|
||||
use common::define::setting::{MAX_RULES, MAX_RULES_PORT};
|
||||
use common::model::event::{IPv4Event, IPv6Event};
|
||||
use common::model::ip_address::{IPv4, IPv6, Port};
|
||||
|
||||
#[map]
|
||||
static IPV4_SRC_WHITELIST: HashMap<IPv4, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SRC_WHITELIST: HashMap<IPv6, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_DST_WHITELIST: HashMap<IPv4, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_DST_WHITELIST: HashMap<IPv6, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_SRC_BLACKLIST: HashMap<IPv4, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SRC_BLACKLIST: HashMap<IPv6, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_DST_BLACKLIST: HashMap<IPv4, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_DST_BLACKLIST: HashMap<IPv6, [Port; MAX_RULES_PORT]> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
|
||||
pub fn ipv4_is_whitelisted(event: &IPv4Event) -> bool {
|
||||
unsafe {
|
||||
if let Some(ports) = IPV4_SRC_WHITELIST.get(&event.src_ip) {
|
||||
if is_port_exist(ports, event.src_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(ports) = IPV4_DST_WHITELIST.get(&event.dst_ip) {
|
||||
if is_port_exist(ports, event.dst_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn ipv6_is_whitelisted(event: &IPv6Event) -> bool {
|
||||
unsafe {
|
||||
if let Some(ports) = IPV6_SRC_WHITELIST.get(&event.src_ip) {
|
||||
if is_port_exist(ports, event.src_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(ports) = IPV6_DST_WHITELIST.get(&event.dst_ip) {
|
||||
if is_port_exist(ports, event.dst_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn ipv4_is_blacklisted(event: &IPv4Event) -> bool {
|
||||
unsafe {
|
||||
if let Some(ports) = IPV4_SRC_BLACKLIST.get(&event.src_ip) {
|
||||
if is_port_exist(ports, event.src_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(ports) = IPV4_DST_BLACKLIST.get(&event.dst_ip) {
|
||||
if is_port_exist(ports, event.dst_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn ipv6_is_blacklisted(event: &IPv6Event) -> bool {
|
||||
unsafe {
|
||||
if let Some(ports) = IPV6_SRC_BLACKLIST.get(&event.src_ip) {
|
||||
if is_port_exist(ports, event.src_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(ports) = IPV6_DST_BLACKLIST.get(&event.dst_ip) {
|
||||
if is_port_exist(ports, event.dst_port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_port_exist(ports: &[Port; MAX_RULES_PORT], target_port: Port) -> bool {
|
||||
if ports.get(0) == Some(&0) {
|
||||
return true;
|
||||
}
|
||||
for &port in ports.iter() {
|
||||
if port == 0 {
|
||||
break;
|
||||
}
|
||||
if port == target_port {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
3
ingress-ebpf/src/action/mod.rs
Normal file
3
ingress-ebpf/src/action/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod access_control;
|
||||
pub mod service;
|
||||
pub mod statistics;
|
||||
163
ingress-ebpf/src/action/service.rs
Normal file
163
ingress-ebpf/src/action/service.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::{Array, HashMap};
|
||||
use common::define::offset::*;
|
||||
use common::define::setting::MAX_RULES;
|
||||
use common::model::event::{IPv4Event, IPv6Event};
|
||||
use common::model::http_method::HttpMethodBitmap;
|
||||
use common::model::ip_address::*;
|
||||
use common::model::placeholder::PlaceHolder;
|
||||
use network_types::ip::IpProto;
|
||||
use network_types::tcp::TcpHdr;
|
||||
|
||||
#[map]
|
||||
static IPV4_HTTP_SERVICE: HashMap<AddrPortV4, HttpMethodBitmap> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_HTTP_SERVICE: HashMap<AddrPortV6, HttpMethodBitmap> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static SSH_WHITE_LIST_ENABLE: Array<PlaceHolder> = Array::with_max_entries(1, 0);
|
||||
#[map]
|
||||
static IPV4_SSH_SERVICE: HashMap<AddrPortV4, PlaceHolder> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SSH_SERVICE: HashMap<AddrPortV6, PlaceHolder> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_SSH_WHITE_LIST: HashMap<IPv4, PlaceHolder> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SSH_WHITE_LIST: HashMap<IPv6, PlaceHolder> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV4_SSH_BLACK_LIST: HashMap<IPv4, PlaceHolder> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
#[map]
|
||||
static IPV6_SSH_BLACK_LIST: HashMap<IPv6, PlaceHolder> = HashMap::with_max_entries(MAX_RULES as u32, 0);
|
||||
|
||||
pub fn ipv4_service_rule_violation(start: usize, end: usize, event: &IPv4Event) -> bool {
|
||||
let protocol = event.protocol;
|
||||
let source = event.source_addr();
|
||||
let destination = event.destination_addr();
|
||||
ipv4_http_service_violation(start, end, &protocol, &destination)
|
||||
|| ipv4_ssh_service_violation(&source, &destination)
|
||||
}
|
||||
|
||||
pub fn ipv6_service_rule_violation(start: usize, end: usize, event: &IPv6Event) -> bool {
|
||||
let protocol = event.protocol;
|
||||
let source = event.source_addr();
|
||||
let destination = event.destination_addr();
|
||||
ipv6_http_service_violation(start, end, &protocol, &destination)
|
||||
|| ipv6_ssh_service_violation(&source, &destination)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv4_http_service_violation(start: usize, end: usize, protocol: &IpProto, destination: &AddrPortV4) -> bool {
|
||||
match IPV4_HTTP_SERVICE.get_ptr_mut(destination) {
|
||||
Some(allow_method) => {
|
||||
if !matches!(protocol, IpProto::Tcp) {
|
||||
return false;
|
||||
}
|
||||
unsafe {
|
||||
if start + IPV4_TCP_HEADER_END > end {
|
||||
return false;
|
||||
}
|
||||
let tcp_header = &*((start + IPV4_TCP_HEADER_START) as *const TcpHdr);
|
||||
if tcp_header.syn() != 0 || tcp_header.rst() != 0 || tcp_header.fin() != 0 {
|
||||
return false;
|
||||
}
|
||||
if tcp_header.psh() == 0 || tcp_header.ack() == 0 {
|
||||
return false;
|
||||
}
|
||||
let doff = tcp_header.doff();
|
||||
if doff < 5 || doff > 15 {
|
||||
return false;
|
||||
}
|
||||
let tcp_header_len = (doff * 4) as usize;
|
||||
let tcp_payload_start = IPV4_TCP_HEADER_END + tcp_header_len;
|
||||
match get_http_request_method(start, end, tcp_payload_start) {
|
||||
Some(http_method) => *allow_method & http_method == 0,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv6_http_service_violation(start: usize, end: usize, protocol: &IpProto, destination: &AddrPortV6) -> bool {
|
||||
match IPV6_HTTP_SERVICE.get_ptr_mut(destination) {
|
||||
Some(allow_method) => {
|
||||
if !matches!(protocol, IpProto::Tcp) {
|
||||
return false;
|
||||
}
|
||||
unsafe {
|
||||
if start + IPV6_TCP_HEADER_END > end {
|
||||
return false;
|
||||
}
|
||||
let tcp_header = &*((start + IPV6_TCP_HEADER_START) as *const TcpHdr);
|
||||
if tcp_header.syn() != 0 || tcp_header.rst() != 0 || tcp_header.fin() != 0 {
|
||||
return false;
|
||||
}
|
||||
if tcp_header.psh() == 0 || tcp_header.ack() == 0 {
|
||||
return false;
|
||||
}
|
||||
let doff = tcp_header.doff();
|
||||
if doff < 5 || doff > 15 {
|
||||
return false;
|
||||
}
|
||||
let tcp_header_len = (doff * 4) as usize;
|
||||
let tcp_payload_start = IPV6_TCP_HEADER_END + tcp_header_len;
|
||||
match get_http_request_method(start, end, tcp_payload_start) {
|
||||
Some(http_method) => *allow_method & http_method == 0,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_http_request_method(start: usize, end: usize, offset: usize) -> Option<HttpMethodBitmap> {
|
||||
if start + offset + 8 > end {
|
||||
return None;
|
||||
}
|
||||
let data = unsafe { core::slice::from_raw_parts((start + offset) as *const u8, 8) };
|
||||
match &data[..4] {
|
||||
b"GET " => Some(1 << 0),
|
||||
b"POST" if &data[4..5] == b" " => Some(1 << 1),
|
||||
b"PUT " => Some(1 << 2),
|
||||
b"DELE" if &data[4..7] == b"TE " => Some(1 << 3),
|
||||
b"HEAD" if &data[4..5] == b" " => Some(1 << 4),
|
||||
b"OPTI" if &data[4..8] == b"ONS " => Some(1 << 5),
|
||||
b"PATC" if &data[4..6] == b"H " => Some(1 << 6),
|
||||
b"TRAC" if &data[4..6] == b"E " => Some(1 << 7),
|
||||
b"CONN" if &data[4..8] == b"ECT " => Some(1 << 8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv4_ssh_service_violation(source: &AddrPortV4, destination: &AddrPortV4) -> bool {
|
||||
unsafe {
|
||||
if IPV4_SSH_SERVICE.get(destination).is_some() {
|
||||
if SSH_WHITE_LIST_ENABLE.get(0).is_some() {
|
||||
IPV4_SSH_WHITE_LIST.get(&source.ip()).is_none()
|
||||
} else {
|
||||
IPV4_SSH_BLACK_LIST.get(&source.ip()).is_some()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ipv6_ssh_service_violation(source_ip: &AddrPortV6, destination: &AddrPortV6) -> bool {
|
||||
unsafe {
|
||||
if IPV6_SSH_SERVICE.get(destination).is_some() {
|
||||
if SSH_WHITE_LIST_ENABLE.get(0).is_some() {
|
||||
IPV6_SSH_WHITE_LIST.get(&source_ip.ip()).is_none()
|
||||
} else {
|
||||
IPV6_SSH_BLACK_LIST.get(&source_ip.ip()).is_some()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
93
ingress-ebpf/src/action/statistics.rs
Normal file
93
ingress-ebpf/src/action/statistics.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use aya_ebpf::macros::map;
|
||||
use aya_ebpf::maps::LruHashMap;
|
||||
use common::define::setting::MAX_STATS;
|
||||
use common::model::event::{IPv4Event, IPv6Event};
|
||||
use common::model::flow_stats::FlowStats;
|
||||
use common::model::ip_address::{AddrPortV4, AddrPortV6};
|
||||
|
||||
#[map]
|
||||
static IPV4_INGRESS_SRC_1MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_INGRESS_SRC_10MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_INGRESS_SRC_1HOUR: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_INGRESS_SRC_1MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_INGRESS_SRC_10MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_INGRESS_SRC_1HOUR: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_INGRESS_DST_1MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_INGRESS_DST_10MIN: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV4_INGRESS_DST_1HOUR: LruHashMap<AddrPortV4, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_INGRESS_DST_1MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_INGRESS_DST_10MIN: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
#[map]
|
||||
static IPV6_INGRESS_DST_1HOUR: LruHashMap<AddrPortV6, FlowStats> = LruHashMap::with_max_entries(MAX_STATS as u32, 0);
|
||||
|
||||
pub fn ipv4_update_stats(event: &IPv4Event) {
|
||||
unsafe {
|
||||
let source = event.source_addr();
|
||||
let destination = event.destination_addr();
|
||||
ipv4_update_flow_stats(&IPV4_INGRESS_SRC_1MIN, &source, event);
|
||||
ipv4_update_flow_stats(&IPV4_INGRESS_SRC_10MIN, &source, event);
|
||||
ipv4_update_flow_stats(&IPV4_INGRESS_SRC_1HOUR, &source, event);
|
||||
ipv4_update_flow_stats(&IPV4_INGRESS_DST_1MIN, &destination, event);
|
||||
ipv4_update_flow_stats(&IPV4_INGRESS_DST_10MIN, &destination, event);
|
||||
ipv4_update_flow_stats(&IPV4_INGRESS_DST_1HOUR, &destination, event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipv6_update_stats(event: &IPv6Event) {
|
||||
unsafe {
|
||||
let source = event.source_addr();
|
||||
let destination = event.destination_addr();
|
||||
ipv6_update_flow_status(&IPV6_INGRESS_SRC_1MIN, &source, event);
|
||||
ipv6_update_flow_status(&IPV6_INGRESS_SRC_10MIN, &source, event);
|
||||
ipv6_update_flow_status(&IPV6_INGRESS_SRC_1HOUR, &source, event);
|
||||
ipv6_update_flow_status(&IPV6_INGRESS_DST_1MIN, &destination, event);
|
||||
ipv6_update_flow_status(&IPV6_INGRESS_DST_10MIN, &destination, event);
|
||||
ipv6_update_flow_status(&IPV6_INGRESS_DST_1HOUR, &destination, event);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn ipv4_update_flow_stats(map: &LruHashMap<AddrPortV4, FlowStats>, key: &AddrPortV4, event: &IPv4Event) {
|
||||
unsafe {
|
||||
if let Some(status) = map.get_ptr_mut(key) {
|
||||
(*status).bytes += event.packet_length as u64;
|
||||
(*status).packets += 1;
|
||||
(*status).last_seen = event.timestamp_us;
|
||||
} else {
|
||||
let new_stats = FlowStats {
|
||||
bytes: event.packet_length as u64,
|
||||
packets: 1,
|
||||
last_seen: event.timestamp_us,
|
||||
};
|
||||
let _ = map.insert(key, &new_stats, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn ipv6_update_flow_status(map: &LruHashMap<AddrPortV6, FlowStats>, key: &AddrPortV6, event: &IPv6Event) {
|
||||
unsafe {
|
||||
if let Some(status) = map.get_ptr_mut(key) {
|
||||
(*status).bytes += event.packet_length as u64;
|
||||
(*status).packets += 1;
|
||||
(*status).last_seen = event.timestamp_us;
|
||||
} else {
|
||||
let new_stats = FlowStats {
|
||||
bytes: event.packet_length as u64,
|
||||
packets: 1,
|
||||
last_seen: event.timestamp_us,
|
||||
};
|
||||
let _ = map.insert(key, &new_stats, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
ingress-ebpf/src/lib.rs
Normal file
1
ingress-ebpf/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
#![no_std]
|
||||
164
ingress-ebpf/src/main.rs
Normal file
164
ingress-ebpf/src/main.rs
Normal file
@ -0,0 +1,164 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
mod action;
|
||||
|
||||
use aya_ebpf::bindings::xdp_action;
|
||||
use aya_ebpf::macros::{map, xdp};
|
||||
use aya_ebpf::maps::{PerCpuArray, ProgramArray, XskMap};
|
||||
use aya_ebpf::programs::XdpContext;
|
||||
#[allow(unused_imports)]
|
||||
use aya_log_ebpf::info;
|
||||
use common::define::program_array::ingress::*;
|
||||
use common::ebpf::parsing;
|
||||
use common::model::event::Event;
|
||||
|
||||
use crate::action::{access_control, service, statistics};
|
||||
|
||||
#[map]
|
||||
static PROGRAM_ARRAY: ProgramArray = ProgramArray::with_max_entries(8, 0);
|
||||
#[map]
|
||||
static PARSED_PACKET: PerCpuArray<Event> = PerCpuArray::with_max_entries(1, 0);
|
||||
#[map]
|
||||
static INGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0);
|
||||
|
||||
#[xdp]
|
||||
pub fn net_guardia(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
let _ = packet_intake(&ctx);
|
||||
let _ = PROGRAM_ARRAY.tail_call(&ctx, TRANSMISSION);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn packet_intake(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let start = ctx.data();
|
||||
let end = ctx.data_end();
|
||||
let ptr = PARSED_PACKET.get_ptr_mut(0).ok_or(())?;
|
||||
parsing::parse_packet(start, end, ptr)?;
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, ACCESS_CONTROL);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn access_control(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
match try_access_control(&ctx) {
|
||||
Ok(action) => action,
|
||||
Err(_) => {
|
||||
let _ = PROGRAM_ARRAY.tail_call(&ctx, TRANSMISSION);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn try_access_control(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let parsed_packet = &*ptr;
|
||||
match parsed_packet {
|
||||
Event::IPv4(event) => {
|
||||
if access_control::ipv4_is_whitelisted(event) {
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS);
|
||||
return Err(());
|
||||
}
|
||||
if access_control::ipv4_is_blacklisted(event) {
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
Event::IPv6(event) => {
|
||||
if access_control::ipv6_is_whitelisted(event) {
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS);
|
||||
return Err(());
|
||||
}
|
||||
if access_control::ipv6_is_blacklisted(event) {
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, SERVICE);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn service(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
match try_service(&ctx) {
|
||||
Ok(action) => action,
|
||||
Err(_) => {
|
||||
let _ = PROGRAM_ARRAY.tail_call(&ctx, TRANSMISSION);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn try_service(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let start = ctx.data();
|
||||
let end = ctx.data_end();
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let parsed_packet = &*ptr;
|
||||
match parsed_packet {
|
||||
Event::IPv4(event) => {
|
||||
if service::ipv4_service_rule_violation(start, end, event) {
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
Event::IPv6(event) => {
|
||||
if service::ipv6_service_rule_violation(start, end, event) {
|
||||
return Ok(xdp_action::XDP_DROP);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn statistics(ctx: XdpContext) -> u32 {
|
||||
unsafe {
|
||||
let _ = try_statistics(&ctx);
|
||||
xdp_action::XDP_PASS
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
unsafe fn try_statistics(ctx: &XdpContext) -> Result<u32, ()> {
|
||||
unsafe {
|
||||
let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?;
|
||||
let parsed_packet = &*ptr;
|
||||
match parsed_packet {
|
||||
Event::IPv4(event) => {
|
||||
statistics::ipv4_update_stats(&event);
|
||||
}
|
||||
Event::IPv6(event) => {
|
||||
statistics::ipv6_update_stats(&event);
|
||||
}
|
||||
}
|
||||
let _ = PROGRAM_ARRAY.tail_call(ctx, TRANSMISSION);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
#[xdp]
|
||||
pub fn transmission(ctx: XdpContext) -> u32 {
|
||||
let queue_id = unsafe { (*ctx.ctx).rx_queue_index };
|
||||
match INGRESS_XSKS_MAP.redirect(queue_id, 0) {
|
||||
Ok(action) => action,
|
||||
Err(_) => xdp_action::XDP_PASS,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
unsafe { core::hint::unreachable_unchecked() }
|
||||
}
|
||||
12
macros/Cargo.toml
Normal file
12
macros/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "macros"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
220
macros/src/error_enum.rs
Normal file
220
macros/src/error_enum.rs
Normal file
@ -0,0 +1,220 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Attribute, Error, Expr, Ident, LitStr, Result, Token, Type};
|
||||
|
||||
pub struct ErrorVariant {
|
||||
pub attributes: Vec<Attribute>,
|
||||
pub error_msg: LitStr,
|
||||
pub name: Ident,
|
||||
pub fields: Vec<(Ident, Type)>,
|
||||
pub level: Expr,
|
||||
}
|
||||
|
||||
impl ErrorVariant {
|
||||
pub fn has_no_source(&self) -> bool {
|
||||
self.attributes.iter().any(|attr| attr.path().is_ident("no_source"))
|
||||
}
|
||||
|
||||
pub fn should_generate_constructor(&self, force_no_source: bool) -> bool {
|
||||
if force_no_source || self.has_no_source() {
|
||||
!self.fields.is_empty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ErrorEnumInput {
|
||||
pub enum_name: Ident,
|
||||
pub variants: Vec<ErrorVariant>,
|
||||
}
|
||||
|
||||
impl Parse for ErrorEnumInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let enum_name = input.parse::<Ident>()?;
|
||||
|
||||
let content;
|
||||
syn::braced!(content in input);
|
||||
|
||||
let mut variants = Vec::new();
|
||||
|
||||
while !content.is_empty() {
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
while content.peek(Token![#]) {
|
||||
attributes.push(content.call(Attribute::parse_outer)?);
|
||||
}
|
||||
|
||||
let attributes: Vec<_> = attributes.into_iter().flatten().collect();
|
||||
|
||||
let error_attr = attributes
|
||||
.iter()
|
||||
.find(|attr| attr.path().is_ident("error"))
|
||||
.ok_or_else(|| Error::new(content.span(), "Missing #[error] attribute"))?;
|
||||
|
||||
let error_msg = match &error_attr.meta {
|
||||
syn::Meta::List(list) => syn::parse2::<LitStr>(list.tokens.clone())?,
|
||||
_ => {
|
||||
return Err(Error::new(error_attr.span(), "Invalid error attribute format"));
|
||||
}
|
||||
};
|
||||
|
||||
let name = content.parse::<Ident>()?;
|
||||
|
||||
let mut fields = Vec::new();
|
||||
if content.peek(syn::token::Brace) {
|
||||
let fields_content;
|
||||
syn::braced!(fields_content in content);
|
||||
|
||||
while !fields_content.is_empty() {
|
||||
let field_name = fields_content.parse::<Ident>()?;
|
||||
fields_content.parse::<Token![:]>()?;
|
||||
let field_type = fields_content.parse::<Type>()?;
|
||||
fields.push((field_name, field_type));
|
||||
|
||||
if !fields_content.is_empty() {
|
||||
fields_content.parse::<Token![,]>()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content.parse::<Token![=>]>()?;
|
||||
let level = content.parse::<Expr>()?;
|
||||
|
||||
if !content.is_empty() {
|
||||
content.parse::<Token![,]>()?;
|
||||
}
|
||||
|
||||
variants.push(ErrorVariant {
|
||||
attributes,
|
||||
error_msg,
|
||||
name,
|
||||
fields,
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ErrorEnumInput { enum_name, variants })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_error_enum(input: TokenStream, force_no_source: bool) -> TokenStream {
|
||||
let input = syn::parse_macro_input!(input as ErrorEnumInput);
|
||||
|
||||
let enum_name = &input.enum_name;
|
||||
let variants = &input.variants;
|
||||
|
||||
let enum_variants = variants.iter().map(|variant| {
|
||||
let name = &variant.name;
|
||||
let error_msg = &variant.error_msg;
|
||||
let fields = &variant.fields;
|
||||
|
||||
let field_definitions = fields.iter().map(|(name, ty)| {
|
||||
quote! { #name: #ty }
|
||||
});
|
||||
|
||||
if force_no_source || variant.has_no_source() {
|
||||
if variant.fields.is_empty() {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name { #(#field_definitions,)* }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[error(#error_msg)]
|
||||
#name {
|
||||
#(#field_definitions,)*
|
||||
err: String
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let level_match_arms = variants.iter().map(|variant| {
|
||||
let name = &variant.name;
|
||||
let level = &variant.level;
|
||||
|
||||
if force_no_source || variant.has_no_source() {
|
||||
if variant.fields.is_empty() {
|
||||
quote! {
|
||||
Self::#name => #level
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Self::#name { .. } => #level
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Self::#name { err: _, .. } => #level
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let constructors = variants.iter().filter_map(|variant| {
|
||||
if !variant.should_generate_constructor(force_no_source) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = &variant.name;
|
||||
let fields = &variant.fields;
|
||||
|
||||
let params = fields.iter().map(|(field_name, field_type)| {
|
||||
quote! { #field_name: impl Into<#field_type> }
|
||||
});
|
||||
|
||||
let field_assignments = fields.iter().map(|(field_name, _)| {
|
||||
quote! { #field_name: #field_name.into() }
|
||||
});
|
||||
|
||||
if force_no_source || variant.has_no_source() {
|
||||
Some(quote! {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #name(#(#params),*) -> Self {
|
||||
Self::#name {
|
||||
#(#field_assignments,)*
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(quote! {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #name(#(#params,)* source: impl std::fmt::Display) -> Self {
|
||||
Self::#name {
|
||||
#(#field_assignments,)*
|
||||
err: source.to_string()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let expanded = quote! {
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
|
||||
pub enum #enum_name {
|
||||
#(#enum_variants,)*
|
||||
}
|
||||
|
||||
impl #enum_name {
|
||||
#[allow(dead_code)]
|
||||
pub fn level(&self) -> tracing::Level {
|
||||
match self {
|
||||
#(#level_match_arms,)*
|
||||
}
|
||||
}
|
||||
|
||||
#(#constructors)*
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
21
macros/src/lib.rs
Normal file
21
macros/src/lib.rs
Normal file
@ -0,0 +1,21 @@
|
||||
mod error_enum;
|
||||
mod log;
|
||||
mod loggable;
|
||||
mod traceable;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn log(input: TokenStream) -> TokenStream {
|
||||
log::log_impl(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn loggable(input: TokenStream) -> TokenStream {
|
||||
loggable::loggable_impl(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn traceable(input: TokenStream) -> TokenStream {
|
||||
traceable::traceable_impl(input)
|
||||
}
|
||||
66
macros/src/log.rs
Normal file
66
macros/src/log.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{parse_macro_input, Expr, Token};
|
||||
|
||||
struct LogInput {
|
||||
error: Expr,
|
||||
debug_info: Option<Expr>,
|
||||
}
|
||||
|
||||
impl Parse for LogInput {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let error = input.parse::<Expr>()?;
|
||||
|
||||
let debug_info = if input.peek(Token![,]) {
|
||||
input.parse::<Token![,]>()?;
|
||||
Some(input.parse::<Expr>()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(LogInput { error, debug_info })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_impl(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as LogInput);
|
||||
|
||||
let error_expr = &input.error;
|
||||
|
||||
if let Some(debug_info) = &input.debug_info {
|
||||
quote! {
|
||||
{
|
||||
let error = #error_expr;
|
||||
let level = error.level();
|
||||
let message = error.to_string();
|
||||
let debug_info = #debug_info;
|
||||
|
||||
match level {
|
||||
tracing::Level::ERROR => tracing::error!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::WARN => tracing::warn!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::INFO => tracing::info!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::DEBUG => tracing::debug!(message = %message, debug = ?debug_info),
|
||||
tracing::Level::TRACE => tracing::trace!(message = %message, debug = ?debug_info),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
{
|
||||
let error = #error_expr;
|
||||
let level = error.level();
|
||||
let message = error.to_string();
|
||||
|
||||
match level {
|
||||
tracing::Level::ERROR => tracing::error!("{}", message),
|
||||
tracing::Level::WARN => tracing::warn!("{}", message),
|
||||
tracing::Level::INFO => tracing::info!("{}", message),
|
||||
tracing::Level::DEBUG => tracing::debug!("{}", message),
|
||||
tracing::Level::TRACE => tracing::trace!("{}", message),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
7
macros/src/loggable.rs
Normal file
7
macros/src/loggable.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
use crate::error_enum;
|
||||
|
||||
pub fn loggable_impl(input: TokenStream) -> TokenStream {
|
||||
error_enum::generate_error_enum(input, true)
|
||||
}
|
||||
7
macros/src/traceable.rs
Normal file
7
macros/src/traceable.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
use crate::error_enum;
|
||||
|
||||
pub fn traceable_impl(input: TokenStream) -> TokenStream {
|
||||
error_enum::generate_error_enum(input, false)
|
||||
}
|
||||
BIN
models/classifier.onnx
Normal file
BIN
models/classifier.onnx
Normal file
Binary file not shown.
BIN
models/deep_autoencoder.onnx
Normal file
BIN
models/deep_autoencoder.onnx
Normal file
Binary file not shown.
289
models/full_config.json
Normal file
289
models/full_config.json
Normal file
@ -0,0 +1,289 @@
|
||||
{
|
||||
"created_at": "2026-03-05T14:04:08.736531",
|
||||
"framework": "PyTorch",
|
||||
"model": {
|
||||
"deep_autoencoder": {
|
||||
"file": "deep_autoencoder.onnx",
|
||||
"input_dim": 27,
|
||||
"encoding_dim": 16,
|
||||
"ae_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts"
|
||||
],
|
||||
"ae_threshold": 0.17166012525558472
|
||||
},
|
||||
"classifier": {
|
||||
"file": "classifier.onnx",
|
||||
"type": "ResNet MLP",
|
||||
"n_features": 28,
|
||||
"n_classes": 5,
|
||||
"classifier_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts",
|
||||
"ae_anomaly_score"
|
||||
]
|
||||
}
|
||||
},
|
||||
"preprocessing": {
|
||||
"ae_clip_params": {
|
||||
"flow_duration": {
|
||||
"lower": 1.0,
|
||||
"upper": 118485085.19000001
|
||||
},
|
||||
"fwd_packets": {
|
||||
"lower": 0.0,
|
||||
"upper": 54.0
|
||||
},
|
||||
"bwd_packets": {
|
||||
"lower": 0.0,
|
||||
"upper": 82.0
|
||||
},
|
||||
"fwd_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 6960.0
|
||||
},
|
||||
"bwd_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 89558.6950000003
|
||||
},
|
||||
"flow_bytes_per_sec": {
|
||||
"lower": 0.0,
|
||||
"upper": 12500000.0
|
||||
},
|
||||
"flow_pkts_per_sec": {
|
||||
"lower": 0.0226829350855,
|
||||
"upper": 2000000.0
|
||||
},
|
||||
"fwd_win_bytes": {
|
||||
"lower": -1.0,
|
||||
"upper": 65535.0
|
||||
},
|
||||
"bwd_win_bytes": {
|
||||
"lower": -1.0,
|
||||
"upper": 64000.0
|
||||
},
|
||||
"fwd_pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 744.7272727272729
|
||||
},
|
||||
"bwd_pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 1393.9601153594992
|
||||
},
|
||||
"fwd_iat_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 86366722.035
|
||||
},
|
||||
"bwd_iat_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 21700000.0
|
||||
},
|
||||
"flow_iat_mean": {
|
||||
"lower": 1.0,
|
||||
"upper": 86353400.64500001
|
||||
},
|
||||
"pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 945.0294113974604
|
||||
},
|
||||
"dst_port": {
|
||||
"lower": 0.0,
|
||||
"upper": 64499.0
|
||||
},
|
||||
"protocol": {
|
||||
"lower": 0.0,
|
||||
"upper": 17.0
|
||||
},
|
||||
"psh_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 240.0
|
||||
},
|
||||
"ack_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 655.0
|
||||
},
|
||||
"syn_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 4.0
|
||||
},
|
||||
"fin_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 2.0
|
||||
},
|
||||
"rst_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 1.0
|
||||
},
|
||||
"pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 943.9221842600019
|
||||
},
|
||||
"fwd_pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 721.4199648708689
|
||||
},
|
||||
"bwd_pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 911.0272448420104
|
||||
},
|
||||
"fwd_seg_size_min": {
|
||||
"lower": 0.0,
|
||||
"upper": 40.0
|
||||
},
|
||||
"fwd_act_data_pkts": {
|
||||
"lower": 0.0,
|
||||
"upper": 58.0
|
||||
}
|
||||
},
|
||||
"ae_scaler": {
|
||||
"mean": [
|
||||
9783470.902880548,
|
||||
3.6532605393470496,
|
||||
3.2219949045990472,
|
||||
311.35281195367173,
|
||||
1407.508354811673,
|
||||
298997.28705051675,
|
||||
32378.36735569276,
|
||||
6158.3256759444475,
|
||||
6125.023325143747,
|
||||
58.1683929498305,
|
||||
149.72168204849987,
|
||||
2991407.86700246,
|
||||
543337.1970583026,
|
||||
2727511.340181723,
|
||||
105.03829359866097,
|
||||
13463.473246959382,
|
||||
8.920585011866685,
|
||||
5.457814320208867,
|
||||
12.214631683524651,
|
||||
0.4885976379312313,
|
||||
0.246598391240437,
|
||||
0.08478201266637703,
|
||||
151.71809799252043,
|
||||
76.89649795414182,
|
||||
143.21014925828936,
|
||||
21.447338538177014,
|
||||
3.2607463735155715
|
||||
],
|
||||
"std": [
|
||||
27895851.239715174,
|
||||
6.4263677035863696,
|
||||
8.326275195918974,
|
||||
739.2548064174216,
|
||||
7898.711708881367,
|
||||
1187804.3239195002,
|
||||
190280.1548511611,
|
||||
15556.664474188405,
|
||||
17012.235322987635,
|
||||
98.89458775902833,
|
||||
242.79522892109904,
|
||||
13146157.964503227,
|
||||
2242915.166631539,
|
||||
12878527.145353919,
|
||||
151.95316919115754,
|
||||
21264.915159327567,
|
||||
4.943209145195101,
|
||||
23.02750953458606,
|
||||
57.0633472601567,
|
||||
1.2662749497077417,
|
||||
0.4553262784517569,
|
||||
0.278557037237646,
|
||||
211.1373807979287,
|
||||
143.47790203327,
|
||||
228.24531573376805,
|
||||
9.323458649060111,
|
||||
7.228913366949205
|
||||
],
|
||||
"feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts"
|
||||
]
|
||||
},
|
||||
"post_scaling_clip": {
|
||||
"min": -5.0,
|
||||
"max": 5.0
|
||||
}
|
||||
},
|
||||
"attack_labels": {
|
||||
"0": "Brute Force",
|
||||
"1": "DDoS",
|
||||
"2": "DoS",
|
||||
"3": "Exploitation",
|
||||
"4": "Reconnaissance"
|
||||
}
|
||||
}
|
||||
239
models/inference_config.json
Normal file
239
models/inference_config.json
Normal file
@ -0,0 +1,239 @@
|
||||
{
|
||||
"ae_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts"
|
||||
],
|
||||
"ae_clip_params": {
|
||||
"flow_duration": {
|
||||
"lower": 1.0,
|
||||
"upper": 118485085.19000001
|
||||
},
|
||||
"fwd_packets": {
|
||||
"lower": 0.0,
|
||||
"upper": 54.0
|
||||
},
|
||||
"bwd_packets": {
|
||||
"lower": 0.0,
|
||||
"upper": 82.0
|
||||
},
|
||||
"fwd_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 6960.0
|
||||
},
|
||||
"bwd_bytes": {
|
||||
"lower": 0.0,
|
||||
"upper": 89558.6950000003
|
||||
},
|
||||
"flow_bytes_per_sec": {
|
||||
"lower": 0.0,
|
||||
"upper": 12500000.0
|
||||
},
|
||||
"flow_pkts_per_sec": {
|
||||
"lower": 0.0226829350855,
|
||||
"upper": 2000000.0
|
||||
},
|
||||
"fwd_win_bytes": {
|
||||
"lower": -1.0,
|
||||
"upper": 65535.0
|
||||
},
|
||||
"bwd_win_bytes": {
|
||||
"lower": -1.0,
|
||||
"upper": 64000.0
|
||||
},
|
||||
"fwd_pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 744.7272727272729
|
||||
},
|
||||
"bwd_pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 1393.9601153594992
|
||||
},
|
||||
"fwd_iat_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 86366722.035
|
||||
},
|
||||
"bwd_iat_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 21700000.0
|
||||
},
|
||||
"flow_iat_mean": {
|
||||
"lower": 1.0,
|
||||
"upper": 86353400.64500001
|
||||
},
|
||||
"pkt_len_mean": {
|
||||
"lower": 0.0,
|
||||
"upper": 945.0294113974604
|
||||
},
|
||||
"dst_port": {
|
||||
"lower": 0.0,
|
||||
"upper": 64499.0
|
||||
},
|
||||
"protocol": {
|
||||
"lower": 0.0,
|
||||
"upper": 17.0
|
||||
},
|
||||
"psh_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 240.0
|
||||
},
|
||||
"ack_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 655.0
|
||||
},
|
||||
"syn_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 4.0
|
||||
},
|
||||
"fin_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 2.0
|
||||
},
|
||||
"rst_flag_cnt": {
|
||||
"lower": 0.0,
|
||||
"upper": 1.0
|
||||
},
|
||||
"pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 943.9221842600019
|
||||
},
|
||||
"fwd_pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 721.4199648708689
|
||||
},
|
||||
"bwd_pkt_len_std": {
|
||||
"lower": 0.0,
|
||||
"upper": 911.0272448420104
|
||||
},
|
||||
"fwd_seg_size_min": {
|
||||
"lower": 0.0,
|
||||
"upper": 40.0
|
||||
},
|
||||
"fwd_act_data_pkts": {
|
||||
"lower": 0.0,
|
||||
"upper": 58.0
|
||||
}
|
||||
},
|
||||
"ae_scaler_mean": [
|
||||
9783470.902880548,
|
||||
3.6532605393470496,
|
||||
3.2219949045990472,
|
||||
311.35281195367173,
|
||||
1407.508354811673,
|
||||
298997.28705051675,
|
||||
32378.36735569276,
|
||||
6158.3256759444475,
|
||||
6125.023325143747,
|
||||
58.1683929498305,
|
||||
149.72168204849987,
|
||||
2991407.86700246,
|
||||
543337.1970583026,
|
||||
2727511.340181723,
|
||||
105.03829359866097,
|
||||
13463.473246959382,
|
||||
8.920585011866685,
|
||||
5.457814320208867,
|
||||
12.214631683524651,
|
||||
0.4885976379312313,
|
||||
0.246598391240437,
|
||||
0.08478201266637703,
|
||||
151.71809799252043,
|
||||
76.89649795414182,
|
||||
143.21014925828936,
|
||||
21.447338538177014,
|
||||
3.2607463735155715
|
||||
],
|
||||
"ae_scaler_std": [
|
||||
27895851.239715174,
|
||||
6.4263677035863696,
|
||||
8.326275195918974,
|
||||
739.2548064174216,
|
||||
7898.711708881367,
|
||||
1187804.3239195002,
|
||||
190280.1548511611,
|
||||
15556.664474188405,
|
||||
17012.235322987635,
|
||||
98.89458775902833,
|
||||
242.79522892109904,
|
||||
13146157.964503227,
|
||||
2242915.166631539,
|
||||
12878527.145353919,
|
||||
151.95316919115754,
|
||||
21264.915159327567,
|
||||
4.943209145195101,
|
||||
23.02750953458606,
|
||||
57.0633472601567,
|
||||
1.2662749497077417,
|
||||
0.4553262784517569,
|
||||
0.278557037237646,
|
||||
211.1373807979287,
|
||||
143.47790203327,
|
||||
228.24531573376805,
|
||||
9.323458649060111,
|
||||
7.228913366949205
|
||||
],
|
||||
"ae_post_clip_min": -5.0,
|
||||
"ae_post_clip_max": 5.0,
|
||||
"ae_threshold": 0.17166012525558472,
|
||||
"classifier_feature_names": [
|
||||
"flow_duration",
|
||||
"fwd_packets",
|
||||
"bwd_packets",
|
||||
"fwd_bytes",
|
||||
"bwd_bytes",
|
||||
"flow_bytes_per_sec",
|
||||
"flow_pkts_per_sec",
|
||||
"fwd_win_bytes",
|
||||
"bwd_win_bytes",
|
||||
"fwd_pkt_len_mean",
|
||||
"bwd_pkt_len_mean",
|
||||
"fwd_iat_mean",
|
||||
"bwd_iat_mean",
|
||||
"flow_iat_mean",
|
||||
"pkt_len_mean",
|
||||
"dst_port",
|
||||
"protocol",
|
||||
"psh_flag_cnt",
|
||||
"ack_flag_cnt",
|
||||
"syn_flag_cnt",
|
||||
"fin_flag_cnt",
|
||||
"rst_flag_cnt",
|
||||
"pkt_len_std",
|
||||
"fwd_pkt_len_std",
|
||||
"bwd_pkt_len_std",
|
||||
"fwd_seg_size_min",
|
||||
"fwd_act_data_pkts",
|
||||
"ae_anomaly_score"
|
||||
],
|
||||
"attack_labels": {
|
||||
"0": "Brute Force",
|
||||
"1": "DDoS",
|
||||
"2": "DoS",
|
||||
"3": "Exploitation",
|
||||
"4": "Reconnaissance"
|
||||
}
|
||||
}
|
||||
6
net-guardia-frontend/package-lock.json
generated
Normal file
6
net-guardia-frontend/package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "net-guardia-frontend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
48
net-guardia/Cargo.toml
Normal file
48
net-guardia/Cargo.toml
Normal file
@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "net-guardia"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
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"
|
||||
|
||||
[build-dependencies]
|
||||
cargo_metadata = { workspace = true }
|
||||
dotenvy = "0.15.7"
|
||||
|
||||
[[bin]]
|
||||
name = "net-guardia"
|
||||
path = "src/main.rs"
|
||||
431
net-guardia/build.rs
Normal file
431
net-guardia/build.rs
Normal file
@ -0,0 +1,431 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{BufRead as _, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind};
|
||||
|
||||
fn main() {
|
||||
build_ingress_ebpf();
|
||||
build_egress_ebpf();
|
||||
build_frontend();
|
||||
}
|
||||
|
||||
fn build_ingress_ebpf() {
|
||||
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
|
||||
let ebpf_package = packages
|
||||
.into_iter()
|
||||
.find(|Package { name, .. }| **name == "ingress-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);
|
||||
|
||||
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("../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");
|
||||
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_frontend() {
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
// let Some(frontend_dir) = env::var_os("FRONTEND_DIR") else {
|
||||
// panic!("FRONTEND_DIR environment variable is required but not set");
|
||||
// };
|
||||
|
||||
let project_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let static_dir = project_root.join("static").join("web");
|
||||
|
||||
let project_name = project_root.file_name().unwrap().to_string_lossy();
|
||||
let frontend_dir = project_root
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(format!("{}-frontend", project_name));
|
||||
|
||||
if !frontend_dir.exists() {
|
||||
panic!("Frontend directory {:?} does not exist", frontend_dir);
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed={}", frontend_dir.join("src").display());
|
||||
println!("cargo:rerun-if-changed={}", frontend_dir.join("public").display());
|
||||
println!("cargo:rerun-if-changed={}", frontend_dir.join("package.json").display());
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
frontend_dir.join("package-lock.json").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
frontend_dir.join("next.config.js").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
frontend_dir.join("tailwind.config.js").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
frontend_dir.join("postcss.config.js").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
frontend_dir.join("tsconfig.json").display()
|
||||
);
|
||||
|
||||
let out_dir = frontend_dir.join("out");
|
||||
let need_build = needs_frontend_rebuild(&frontend_dir, &out_dir, &static_dir);
|
||||
if !need_build {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("npm");
|
||||
cmd.arg("install")
|
||||
.current_dir(&frontend_dir);
|
||||
|
||||
let status = cmd
|
||||
.status()
|
||||
.unwrap_or_else(|err| panic!("failed to run npm install: {err}"));
|
||||
if !status.success() {
|
||||
panic!("npm install failed with exit code: {:?}", status.code());
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("npx");
|
||||
cmd.args(["next", "build"])
|
||||
.current_dir(&frontend_dir);
|
||||
|
||||
let status = cmd
|
||||
.status()
|
||||
.unwrap_or_else(|err| panic!("failed to run next build: {err}"));
|
||||
if !status.success() {
|
||||
panic!("next build failed with exit code: {:?}", status.code());
|
||||
}
|
||||
|
||||
if static_dir.exists() {
|
||||
fs::remove_dir_all(&static_dir).unwrap_or_else(|err| panic!("failed to remove {:?}: {err}", static_dir));
|
||||
}
|
||||
fs::create_dir_all(&static_dir).unwrap_or_else(|err| panic!("failed to create {:?}: {err}", static_dir));
|
||||
|
||||
copy_dir_all(&out_dir, &static_dir).unwrap_or_else(|err| panic!("failed to copy frontend build: {err}"));
|
||||
}
|
||||
|
||||
fn needs_frontend_rebuild(frontend_dir: &PathBuf, out_dir: &PathBuf, static_dir: &PathBuf) -> bool {
|
||||
if !out_dir.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !static_dir.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let out_modified = match fs::metadata(out_dir).and_then(|m| m.modified()) {
|
||||
Ok(time) => time,
|
||||
Err(_) => {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
let static_modified = match fs::metadata(static_dir).and_then(|m| m.modified()) {
|
||||
Ok(time) => time,
|
||||
Err(_) => {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
let essential_items = [
|
||||
"src",
|
||||
"public",
|
||||
"package.json",
|
||||
"next.config.js",
|
||||
"tailwind.config.js",
|
||||
"postcss.config.js",
|
||||
"tsconfig.json",
|
||||
"package-lock.json",
|
||||
];
|
||||
|
||||
for item_name in essential_items {
|
||||
let item_path = frontend_dir.join(item_name);
|
||||
if !item_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let item_modified = match get_dir_last_modified(&item_path) {
|
||||
Some(time) => time,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if item_modified > out_modified {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if out_modified > static_modified {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn get_dir_last_modified(path: &PathBuf) -> Option<SystemTime> {
|
||||
if path.is_file() {
|
||||
return fs::metadata(path).and_then(|m| m.modified()).ok();
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
let mut latest = fs::metadata(path).and_then(|m| m.modified()).ok()?;
|
||||
|
||||
if let Ok(entries) = fs::read_dir(path) {
|
||||
for entry in entries.flatten() {
|
||||
if let Some(modified) = get_dir_last_modified(&entry.path()) {
|
||||
if modified > latest {
|
||||
latest = modified;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Some(latest);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> std::io::Result<()> {
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
let src_path = entry.path();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
|
||||
if file_type.is_dir() {
|
||||
fs::create_dir_all(&dst_path)?;
|
||||
copy_dir_all(&src_path, &dst_path)?;
|
||||
} else {
|
||||
fs::copy(&src_path, &dst_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
206
net-guardia/src/core/ebpf/access_control.rs
Normal file
206
net-guardia/src/core/ebpf/access_control.rs
Normal file
@ -0,0 +1,206 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use aya::maps::{HashMap as AyaHashMap, MapData};
|
||||
use aya::{Ebpf, Pod};
|
||||
use common::define::setting::MAX_RULES_PORT;
|
||||
use common::model::ip_address::{IPv4, IPv6, Port};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::model::direction::FlowDirection;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::ip_address::NativeConvert;
|
||||
use crate::model::list_type::ListType;
|
||||
use crate::utils::ip_address::convert_ports_to_vec;
|
||||
|
||||
pub struct AccessControl {
|
||||
ipv4_src_whitelist: RwLock<MapWrapper<IPv4>>,
|
||||
ipv4_src_blacklist: RwLock<MapWrapper<IPv4>>,
|
||||
ipv4_dst_whitelist: RwLock<MapWrapper<IPv4>>,
|
||||
ipv4_dst_blacklist: RwLock<MapWrapper<IPv4>>,
|
||||
ipv6_src_whitelist: RwLock<MapWrapper<IPv6>>,
|
||||
ipv6_src_blacklist: RwLock<MapWrapper<IPv6>>,
|
||||
ipv6_dst_whitelist: RwLock<MapWrapper<IPv6>>,
|
||||
ipv6_dst_blacklist: RwLock<MapWrapper<IPv6>>,
|
||||
}
|
||||
|
||||
impl AccessControl {
|
||||
pub fn new(ebpf: &mut Ebpf) -> Result<Self, Error> {
|
||||
let access_control = Self {
|
||||
ipv4_src_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV4_SRC_WHITELIST")?),
|
||||
ipv4_src_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV4_SRC_BLACKLIST")?),
|
||||
ipv4_dst_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV4_DST_WHITELIST")?),
|
||||
ipv4_dst_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV4_DST_BLACKLIST")?),
|
||||
ipv6_src_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV6_SRC_WHITELIST")?),
|
||||
ipv6_src_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV6_SRC_BLACKLIST")?),
|
||||
ipv6_dst_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV6_DST_WHITELIST")?),
|
||||
ipv6_dst_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV6_DST_BLACKLIST")?),
|
||||
};
|
||||
Ok(access_control)
|
||||
}
|
||||
|
||||
pub async fn get_ipv4_list(&self, direction: FlowDirection, list_type: ListType) -> HashMap<Ipv4Addr, Vec<Port>> {
|
||||
let map_wrapper = match (direction, list_type) {
|
||||
(FlowDirection::Source, ListType::White) => self.ipv4_src_whitelist.read().await,
|
||||
(FlowDirection::Source, ListType::Black) => self.ipv4_src_blacklist.read().await,
|
||||
(FlowDirection::Destination, ListType::White) => self.ipv4_dst_whitelist.read().await,
|
||||
(FlowDirection::Destination, ListType::Black) => self.ipv4_dst_blacklist.read().await,
|
||||
};
|
||||
map_wrapper.get_list()
|
||||
}
|
||||
|
||||
pub async fn get_ipv6_list(&self, direction: FlowDirection, list_type: ListType) -> HashMap<Ipv6Addr, Vec<Port>> {
|
||||
let map_wrapper = match (direction, list_type) {
|
||||
(FlowDirection::Source, ListType::White) => self.ipv6_src_whitelist.read().await,
|
||||
(FlowDirection::Source, ListType::Black) => self.ipv6_src_blacklist.read().await,
|
||||
(FlowDirection::Destination, ListType::White) => self.ipv6_dst_whitelist.read().await,
|
||||
(FlowDirection::Destination, ListType::Black) => self.ipv6_dst_blacklist.read().await,
|
||||
};
|
||||
map_wrapper.get_list()
|
||||
}
|
||||
|
||||
pub async fn add_ipv4_list(
|
||||
&self,
|
||||
direction: FlowDirection,
|
||||
list_type: ListType,
|
||||
address: SocketAddrV4,
|
||||
) -> Result<(), Error> {
|
||||
let ip: u32 = (*address.ip()).to_bits().to_be();
|
||||
let port = address.port();
|
||||
let mut map_wrapper = match (direction, list_type) {
|
||||
(FlowDirection::Source, ListType::White) => self.ipv4_src_whitelist.write().await,
|
||||
(FlowDirection::Source, ListType::Black) => self.ipv4_src_blacklist.write().await,
|
||||
(FlowDirection::Destination, ListType::White) => self.ipv4_dst_whitelist.write().await,
|
||||
(FlowDirection::Destination, ListType::Black) => self.ipv4_dst_blacklist.write().await,
|
||||
};
|
||||
map_wrapper.add(ip, port)
|
||||
}
|
||||
|
||||
pub async fn add_ipv6_list(
|
||||
&self,
|
||||
direction: FlowDirection,
|
||||
list_type: ListType,
|
||||
address: SocketAddrV6,
|
||||
) -> Result<(), Error> {
|
||||
let ip: u128 = (*address.ip()).to_bits().to_be();
|
||||
let port = address.port();
|
||||
let mut map_wrapper = match (direction, list_type) {
|
||||
(FlowDirection::Source, ListType::White) => self.ipv6_src_whitelist.write().await,
|
||||
(FlowDirection::Source, ListType::Black) => self.ipv6_src_blacklist.write().await,
|
||||
(FlowDirection::Destination, ListType::White) => self.ipv6_dst_whitelist.write().await,
|
||||
(FlowDirection::Destination, ListType::Black) => self.ipv6_dst_blacklist.write().await,
|
||||
};
|
||||
map_wrapper.add(ip, port)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv4_list(
|
||||
&self,
|
||||
direction: FlowDirection,
|
||||
list_type: ListType,
|
||||
address: SocketAddrV4,
|
||||
) -> Result<(), Error> {
|
||||
let ip: u32 = (*address.ip()).to_bits().to_be();
|
||||
let port = address.port();
|
||||
let mut map_wrapper = match (direction, list_type) {
|
||||
(FlowDirection::Source, ListType::White) => self.ipv4_src_whitelist.write().await,
|
||||
(FlowDirection::Source, ListType::Black) => self.ipv4_src_blacklist.write().await,
|
||||
(FlowDirection::Destination, ListType::White) => self.ipv4_dst_whitelist.write().await,
|
||||
(FlowDirection::Destination, ListType::Black) => self.ipv4_dst_blacklist.write().await,
|
||||
};
|
||||
map_wrapper.remove(ip, port)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv6_list(
|
||||
&self,
|
||||
direction: FlowDirection,
|
||||
list_type: ListType,
|
||||
address: SocketAddrV6,
|
||||
) -> Result<(), Error> {
|
||||
let ip: u128 = (*address.ip()).to_bits().to_be();
|
||||
let port = address.port();
|
||||
let mut map_wrapper = match (direction, list_type) {
|
||||
(FlowDirection::Source, ListType::White) => self.ipv6_src_whitelist.write().await,
|
||||
(FlowDirection::Source, ListType::Black) => self.ipv6_src_blacklist.write().await,
|
||||
(FlowDirection::Destination, ListType::White) => self.ipv6_dst_whitelist.write().await,
|
||||
(FlowDirection::Destination, ListType::Black) => self.ipv6_dst_blacklist.write().await,
|
||||
};
|
||||
map_wrapper.remove(ip, port)
|
||||
}
|
||||
}
|
||||
|
||||
struct MapWrapper<T> {
|
||||
map: AyaHashMap<MapData, T, [Port; MAX_RULES_PORT]>,
|
||||
}
|
||||
|
||||
impl<T: NativeConvert + Pod> MapWrapper<T> {
|
||||
fn new(ebpf: &mut Ebpf, map_name: &str) -> Result<Self, Error> {
|
||||
let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?;
|
||||
let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(Self { map })
|
||||
}
|
||||
|
||||
fn get_list(&self) -> HashMap<T::Native, Vec<Port>> {
|
||||
self.map
|
||||
.iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(|(key, value)| (key.into_native(), convert_ports_to_vec(value)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add(&mut self, ip: T, port: Port) -> Result<(), Error> {
|
||||
let mut new_ports = [0_u16; MAX_RULES_PORT];
|
||||
if port == 0 {
|
||||
new_ports[0] = 0;
|
||||
} else if let Ok(ports) = self.map.get(&ip, 0) {
|
||||
if ports[0] == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let mut index = None;
|
||||
for (i, &value) in ports.iter().enumerate() {
|
||||
if value == port {
|
||||
return Ok(());
|
||||
}
|
||||
if index.is_none() && value == 0 {
|
||||
index = Some(i);
|
||||
}
|
||||
}
|
||||
if index.is_none() {
|
||||
Err(EbpfError::RuleReachLimit)?;
|
||||
}
|
||||
new_ports.copy_from_slice(&ports);
|
||||
new_ports[index.unwrap()] = port;
|
||||
} else {
|
||||
new_ports[0] = port;
|
||||
}
|
||||
self.map
|
||||
.insert(ip, new_ports, 0)
|
||||
.map_err(EbpfError::MapOperationError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&mut self, ip: T, port: Port) -> Result<(), Error> {
|
||||
if let Ok(mut ports) = self.map.get(&ip, 0) {
|
||||
if port == 0 {
|
||||
self.map.remove(&ip).map_err(EbpfError::MapOperationError)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(index) = ports.iter().position(|&x| x == port) {
|
||||
for i in index..(MAX_RULES_PORT - 1) {
|
||||
ports[i] = ports[i + 1];
|
||||
}
|
||||
ports[MAX_RULES_PORT - 1] = 0;
|
||||
|
||||
if ports[0] == 0 {
|
||||
self.map.remove(&ip).map_err(EbpfError::MapOperationError)?;
|
||||
} else {
|
||||
self.map.insert(ip, ports, 0).map_err(EbpfError::MapOperationError)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(EbpfError::IpDoesNotExist)?
|
||||
}
|
||||
}
|
||||
}
|
||||
69
net-guardia/src/core/ebpf/mod.rs
Normal file
69
net-guardia/src/core/ebpf/mod.rs
Normal file
@ -0,0 +1,69 @@
|
||||
pub mod access_control;
|
||||
pub mod service;
|
||||
pub mod statistics;
|
||||
pub mod xsk_manager;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use aya::Ebpf;
|
||||
use crossbeam::queue::SegQueue;
|
||||
use macros::log;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::core::ebpf::access_control::AccessControl;
|
||||
use crate::core::ebpf::service::Service;
|
||||
use crate::core::ebpf::statistics::Statistics;
|
||||
use crate::core::ebpf::xsk_manager::XskManager;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
use crate::ml::engine::Engine;
|
||||
|
||||
pub struct EbpfServices {
|
||||
pub xsk_manager: Arc<XskManager>,
|
||||
pub access_control: Arc<AccessControl>,
|
||||
pub service: Arc<Service>,
|
||||
pub statistics: Arc<Statistics>,
|
||||
pub shutdowns: SegQueue<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl EbpfServices {
|
||||
pub fn new(
|
||||
app_config: Arc<AppConfig>,
|
||||
ingress_ebpf: &mut Ebpf,
|
||||
egress_ebpf: &mut Ebpf,
|
||||
) -> Result<Self, Error> {
|
||||
let xsk_manager = XskManager::new(app_config.clone(), ingress_ebpf, egress_ebpf)?;
|
||||
let access_control = AccessControl::new(ingress_ebpf)?;
|
||||
let service = Service::new(ingress_ebpf)?;
|
||||
let statistics = Statistics::new(app_config.clone(), ingress_ebpf, egress_ebpf)?;
|
||||
let ebpf_services = Self {
|
||||
xsk_manager: Arc::new(xsk_manager),
|
||||
access_control: Arc::new(access_control),
|
||||
service: Arc::new(service),
|
||||
statistics: Arc::new(statistics),
|
||||
shutdowns: SegQueue::new(),
|
||||
};
|
||||
Ok(ebpf_services)
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>, ml_engine: Arc<Engine>) -> Result<(), Error> {
|
||||
let xsk_manager = self.xsk_manager.clone();
|
||||
let statistics = self.statistics.clone();
|
||||
|
||||
xsk_manager.run(Some(ml_engine), &self.shutdowns)?;
|
||||
|
||||
let statistics_shutdown = statistics.run().await;
|
||||
|
||||
self.shutdowns.push(statistics_shutdown);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn terminate(self: Arc<Self>) {
|
||||
while let Some(shutdown) = self.shutdowns.pop() {
|
||||
if shutdown.send(()).is_err() {
|
||||
log!(SystemError::ShutdownSignalFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
332
net-guardia/src/core/ebpf/service.rs
Normal file
332
net-guardia/src/core/ebpf/service.rs
Normal file
@ -0,0 +1,332 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use aya::maps::{Array as AyaArray, HashMap as AyaHashMap, MapData};
|
||||
use aya::{Ebpf, Pod};
|
||||
use common::model::http_method::{HttpMethod, HttpMethodBitmap};
|
||||
use common::model::ip_address::{AddrPortV4, AddrPortV6, IPv4, IPv6};
|
||||
use common::model::placeholder::PlaceHolder;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::ip_address::NativeConvert;
|
||||
|
||||
pub struct Service {
|
||||
ipv4_http_service: RwLock<HttpServiceWrapper<AddrPortV4>>,
|
||||
ipv6_http_service: RwLock<HttpServiceWrapper<AddrPortV6>>,
|
||||
ssh_white_list_enable: RwLock<WhiteListControl>,
|
||||
ipv4_ssh_service: RwLock<SshServiceWrapper<AddrPortV4>>,
|
||||
ipv6_ssh_service: RwLock<SshServiceWrapper<AddrPortV6>>,
|
||||
ipv4_ssh_white_list: RwLock<SshListWrapper<IPv4>>,
|
||||
ipv6_ssh_white_list: RwLock<SshListWrapper<IPv6>>,
|
||||
ipv4_ssh_black_list: RwLock<SshListWrapper<IPv4>>,
|
||||
ipv6_ssh_black_list: RwLock<SshListWrapper<IPv6>>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(ebpf: &mut Ebpf) -> Result<Self, Error> {
|
||||
let service = Self {
|
||||
ipv4_http_service: RwLock::new(HttpServiceWrapper::new(ebpf, "IPV4_HTTP_SERVICE")?),
|
||||
ipv6_http_service: RwLock::new(HttpServiceWrapper::new(ebpf, "IPV6_HTTP_SERVICE")?),
|
||||
ssh_white_list_enable: RwLock::new(WhiteListControl::new(ebpf, "SSH_WHITE_LIST_ENABLE")?),
|
||||
ipv4_ssh_service: RwLock::new(SshServiceWrapper::new(ebpf, "IPV4_SSH_SERVICE")?),
|
||||
ipv6_ssh_service: RwLock::new(SshServiceWrapper::new(ebpf, "IPV6_SSH_SERVICE")?),
|
||||
ipv4_ssh_white_list: RwLock::new(SshListWrapper::new(ebpf, "IPV4_SSH_WHITE_LIST")?),
|
||||
ipv6_ssh_white_list: RwLock::new(SshListWrapper::new(ebpf, "IPV6_SSH_WHITE_LIST")?),
|
||||
ipv4_ssh_black_list: RwLock::new(SshListWrapper::new(ebpf, "IPV4_SSH_BLACK_LIST")?),
|
||||
ipv6_ssh_black_list: RwLock::new(SshListWrapper::new(ebpf, "IPV6_SSH_BLACK_LIST")?),
|
||||
};
|
||||
Ok(service)
|
||||
}
|
||||
|
||||
pub async fn get_ipv4_http_service(&self) -> HashMap<SocketAddrV4, Vec<HttpMethod>> {
|
||||
self.ipv4_http_service.read().await.get_http_method()
|
||||
}
|
||||
|
||||
pub async fn get_ipv6_http_service(&self) -> HashMap<SocketAddrV6, Vec<HttpMethod>> {
|
||||
self.ipv6_http_service.read().await.get_http_method()
|
||||
}
|
||||
|
||||
pub async fn add_ipv4_http_service(
|
||||
&self,
|
||||
address: SocketAddrV4,
|
||||
http_method: Vec<HttpMethod>,
|
||||
) -> Result<(), Error> {
|
||||
self.ipv4_http_service
|
||||
.write()
|
||||
.await
|
||||
.add_http_service(address, http_method)
|
||||
}
|
||||
|
||||
pub async fn add_ipv6_http_service(
|
||||
&self,
|
||||
address: SocketAddrV6,
|
||||
http_method: Vec<HttpMethod>,
|
||||
) -> Result<(), Error> {
|
||||
self.ipv6_http_service
|
||||
.write()
|
||||
.await
|
||||
.add_http_service(address, http_method)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv4_http_service(
|
||||
&self,
|
||||
address: SocketAddrV4,
|
||||
removed_http_method: Vec<HttpMethod>,
|
||||
) -> Result<(), Error> {
|
||||
self.ipv4_http_service
|
||||
.write()
|
||||
.await
|
||||
.remove_http_service(address, removed_http_method)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv6_http_service(
|
||||
&self,
|
||||
address: SocketAddrV6,
|
||||
removed_http_method: Vec<HttpMethod>,
|
||||
) -> Result<(), Error> {
|
||||
self.ipv6_http_service
|
||||
.write()
|
||||
.await
|
||||
.remove_http_service(address, removed_http_method)
|
||||
}
|
||||
|
||||
pub async fn is_ssh_white_list_enable(&self) -> bool {
|
||||
self.ssh_white_list_enable.read().await.is_white_list_enable()
|
||||
}
|
||||
|
||||
pub async fn enable_ssh_white_list(&self) -> Result<(), Error> {
|
||||
self.ssh_white_list_enable.write().await.enable_white_list()
|
||||
}
|
||||
|
||||
pub async fn disable_ssh_white_list(&self) -> Result<(), Error> {
|
||||
self.ssh_white_list_enable.write().await.disable_white_list()
|
||||
}
|
||||
|
||||
pub async fn get_ipv4_ssh_service(&self) -> Vec<SocketAddrV4> {
|
||||
self.ipv4_ssh_service.read().await.get_ssh_service()
|
||||
}
|
||||
|
||||
pub async fn get_ipv6_ssh_service(&self) -> Vec<SocketAddrV6> {
|
||||
self.ipv6_ssh_service.read().await.get_ssh_service()
|
||||
}
|
||||
|
||||
pub async fn add_ipv4_ssh_service(&self, address: SocketAddrV4) -> Result<(), Error> {
|
||||
self.ipv4_ssh_service.write().await.add_ssh_service(address)
|
||||
}
|
||||
|
||||
pub async fn add_ipv6_ssh_service(&self, address: SocketAddrV6) -> Result<(), Error> {
|
||||
self.ipv6_ssh_service.write().await.add_ssh_service(address)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv4_ssh_service(&self, address: SocketAddrV4) -> Result<(), Error> {
|
||||
self.ipv4_ssh_service.write().await.remove_ssh_service(address)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv6_ssh_service(&self, address: SocketAddrV6) -> Result<(), Error> {
|
||||
self.ipv6_ssh_service.write().await.remove_ssh_service(address)
|
||||
}
|
||||
|
||||
pub async fn get_ipv4_ssh_white_list(&self) -> Vec<Ipv4Addr> {
|
||||
self.ipv4_ssh_white_list.read().await.get_list()
|
||||
}
|
||||
|
||||
pub async fn get_ipv6_ssh_white_list(&self) -> Vec<Ipv6Addr> {
|
||||
self.ipv6_ssh_white_list.read().await.get_list()
|
||||
}
|
||||
|
||||
pub async fn add_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.ipv4_ssh_white_list.write().await.add_list(ip)
|
||||
}
|
||||
|
||||
pub async fn add_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.ipv6_ssh_white_list.write().await.add_list(ip)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.ipv4_ssh_white_list.write().await.remove_list(ip)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.ipv6_ssh_white_list.write().await.remove_list(ip)
|
||||
}
|
||||
|
||||
pub async fn get_ipv4_ssh_black_list(&self) -> Vec<Ipv4Addr> {
|
||||
self.ipv4_ssh_black_list.read().await.get_list()
|
||||
}
|
||||
|
||||
pub async fn get_ipv6_ssh_black_list(&self) -> Vec<Ipv6Addr> {
|
||||
self.ipv6_ssh_black_list.read().await.get_list()
|
||||
}
|
||||
|
||||
pub async fn add_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.ipv4_ssh_black_list.write().await.add_list(ip)
|
||||
}
|
||||
|
||||
pub async fn add_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.ipv6_ssh_black_list.write().await.add_list(ip)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.ipv4_ssh_black_list.write().await.remove_list(ip)
|
||||
}
|
||||
|
||||
pub async fn remove_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.ipv6_ssh_black_list.write().await.remove_list(ip)
|
||||
}
|
||||
}
|
||||
|
||||
struct WhiteListControl {
|
||||
map: AyaArray<MapData, PlaceHolder>,
|
||||
}
|
||||
|
||||
impl WhiteListControl {
|
||||
fn new(ebpf: &mut Ebpf, map_name: &str) -> Result<Self, Error> {
|
||||
let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?;
|
||||
let map = AyaArray::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(Self { map })
|
||||
}
|
||||
|
||||
fn is_white_list_enable(&self) -> bool {
|
||||
match self.map.get(&0, 0) {
|
||||
Ok(status) => {
|
||||
if status == 0 {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_white_list(&mut self) -> Result<(), Error> {
|
||||
self.map.set(0, 1_u8, 0).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disable_white_list(&mut self) -> Result<(), Error> {
|
||||
self.map.set(0, 0_u8, 0).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpServiceWrapper<T> {
|
||||
map: AyaHashMap<MapData, T, HttpMethodBitmap>,
|
||||
}
|
||||
|
||||
impl<T: NativeConvert + Pod> HttpServiceWrapper<T> {
|
||||
fn new(ebpf: &mut Ebpf, map_name: &str) -> Result<Self, Error> {
|
||||
let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?;
|
||||
let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(Self { map })
|
||||
}
|
||||
|
||||
fn get_http_method(&self) -> HashMap<T::Native, Vec<HttpMethod>> {
|
||||
self.map
|
||||
.iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(|(key, value)| {
|
||||
let address = key.into_native();
|
||||
(address, HttpMethod::convert_from_bitmap(value))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_http_service(&mut self, address: T::Native, http_method: Vec<HttpMethod>) -> Result<(), Error> {
|
||||
let address = T::from_native(address);
|
||||
let ebpf_method = HttpMethod::convert_to_bitmap(http_method);
|
||||
self.map
|
||||
.insert(address, ebpf_method, 0)
|
||||
.map_err(|_| EbpfError::RuleReachLimit)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_http_service(&mut self, address: T::Native, removed_http_method: Vec<HttpMethod>) -> Result<(), Error> {
|
||||
let address = T::from_native(address);
|
||||
if let Ok(current_http_method) = self.map.get(&address, 0) {
|
||||
let mut http_method = HttpMethod::convert_from_bitmap(current_http_method);
|
||||
http_method.retain(|method| !removed_http_method.contains(method));
|
||||
if http_method.is_empty() {
|
||||
self.map.remove(&address).map_err(EbpfError::MapOperationError)?;
|
||||
} else {
|
||||
let new_http_method = HttpMethod::convert_to_bitmap(http_method);
|
||||
self.map
|
||||
.insert(&address, new_http_method, 0)
|
||||
.map_err(EbpfError::MapOperationError)?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(EbpfError::IpDoesNotExist)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SshServiceWrapper<T> {
|
||||
map: AyaHashMap<MapData, T, PlaceHolder>,
|
||||
}
|
||||
|
||||
impl<T: NativeConvert + Pod> SshServiceWrapper<T> {
|
||||
fn new(ebpf: &mut Ebpf, map_name: &str) -> Result<Self, Error> {
|
||||
let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?;
|
||||
let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(Self { map })
|
||||
}
|
||||
|
||||
fn get_ssh_service(&self) -> Vec<T::Native> {
|
||||
self.map
|
||||
.keys()
|
||||
.filter_map(Result::ok)
|
||||
.map(|key| key.into_native())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_ssh_service(&mut self, address: T::Native) -> Result<(), Error> {
|
||||
let address = T::from_native(address);
|
||||
self.map
|
||||
.insert(address, 0_u8, 0)
|
||||
.map_err(|_| EbpfError::RuleReachLimit)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_ssh_service(&mut self, address: T::Native) -> Result<(), Error> {
|
||||
let address = T::from_native(address);
|
||||
self.map.remove(&address).map_err(|_| EbpfError::IpDoesNotExist)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct SshListWrapper<T> {
|
||||
map: AyaHashMap<MapData, T, PlaceHolder>,
|
||||
}
|
||||
|
||||
impl<T: NativeConvert + Pod> SshListWrapper<T> {
|
||||
fn new(ebpf: &mut Ebpf, map_name: &str) -> Result<Self, Error> {
|
||||
let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?;
|
||||
let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(Self { map })
|
||||
}
|
||||
|
||||
fn get_list(&self) -> Vec<T::Native> {
|
||||
self.map
|
||||
.keys()
|
||||
.filter_map(Result::ok)
|
||||
.map(|key| key.into_native())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_list(&mut self, address: T::Native) -> Result<(), Error> {
|
||||
let address = T::from_native(address);
|
||||
self.map
|
||||
.insert(address, 0_u8, 0)
|
||||
.map_err(|_| EbpfError::RuleReachLimit)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_list(&mut self, address: T::Native) -> Result<(), Error> {
|
||||
let address = T::from_native(address);
|
||||
self.map.remove(&address).map_err(|_| EbpfError::IpDoesNotExist)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
258
net-guardia/src/core/ebpf/statistics.rs
Normal file
258
net-guardia/src/core/ebpf/statistics.rs
Normal file
@ -0,0 +1,258 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, SocketAddrV4, SocketAddrV6};
|
||||
use std::sync::Arc;
|
||||
|
||||
use aya::maps::{HashMap as AyaHashMap, MapData};
|
||||
use aya::{Ebpf, Pod};
|
||||
use common::model::flow_stats::FlowStats;
|
||||
use common::model::ip_address::{AddrPortV4, AddrPortV6};
|
||||
use futures::future::join_all;
|
||||
use macros::log;
|
||||
use tokio::select;
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::infrastructure::geoip::GeoIpService;
|
||||
use crate::model::direction::{Direction, FlowDirection};
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::geo_stats::FlowStatsWithGeo;
|
||||
use crate::model::ip_address::NativeConvert;
|
||||
use crate::model::time_type::TimeType;
|
||||
use crate::utils::boot_time::boot_time;
|
||||
|
||||
pub struct Statistics {
|
||||
app_config: Arc<AppConfig>,
|
||||
boot_time: u64,
|
||||
geo_ip: Option<Arc<GeoIpService>>,
|
||||
ipv4_maps: HashMap<(Direction, FlowDirection, TimeType), RwLock<FlowMap<AddrPortV4>>>,
|
||||
ipv6_maps: HashMap<(Direction, FlowDirection, TimeType), RwLock<FlowMap<AddrPortV6>>>,
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
const INGRESS_MAPS: [((Direction, FlowDirection, TimeType), (&'static str, &'static str)); 6] = [
|
||||
(
|
||||
(Direction::Ingress, FlowDirection::Source, TimeType::_1Min),
|
||||
("IPV4_INGRESS_SRC_1MIN", "IPV6_INGRESS_SRC_1MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Ingress, FlowDirection::Source, TimeType::_10Min),
|
||||
("IPV4_INGRESS_SRC_10MIN", "IPV6_INGRESS_SRC_10MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Ingress, FlowDirection::Source, TimeType::_1Hour),
|
||||
("IPV4_INGRESS_SRC_1HOUR", "IPV6_INGRESS_SRC_1HOUR"),
|
||||
),
|
||||
(
|
||||
(Direction::Ingress, FlowDirection::Destination, TimeType::_1Min),
|
||||
("IPV4_INGRESS_DST_1MIN", "IPV6_INGRESS_DST_1MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Ingress, FlowDirection::Destination, TimeType::_10Min),
|
||||
("IPV4_INGRESS_DST_10MIN", "IPV6_INGRESS_DST_10MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Ingress, FlowDirection::Destination, TimeType::_1Hour),
|
||||
("IPV4_INGRESS_DST_1HOUR", "IPV6_INGRESS_DST_1HOUR"),
|
||||
),
|
||||
];
|
||||
|
||||
const EGRESS_MAPS: [((Direction, FlowDirection, TimeType), (&'static str, &'static str)); 6] = [
|
||||
(
|
||||
(Direction::Egress, FlowDirection::Source, TimeType::_1Min),
|
||||
("IPV4_EGRESS_SRC_1MIN", "IPV6_EGRESS_SRC_1MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Egress, FlowDirection::Source, TimeType::_10Min),
|
||||
("IPV4_EGRESS_SRC_10MIN", "IPV6_EGRESS_SRC_10MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Egress, FlowDirection::Source, TimeType::_1Hour),
|
||||
("IPV4_EGRESS_SRC_1HOUR", "IPV6_EGRESS_SRC_1HOUR"),
|
||||
),
|
||||
(
|
||||
(Direction::Egress, FlowDirection::Destination, TimeType::_1Min),
|
||||
("IPV4_EGRESS_DST_1MIN", "IPV6_EGRESS_DST_1MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Egress, FlowDirection::Destination, TimeType::_10Min),
|
||||
("IPV4_EGRESS_DST_10MIN", "IPV6_EGRESS_DST_10MIN"),
|
||||
),
|
||||
(
|
||||
(Direction::Egress, FlowDirection::Destination, TimeType::_1Hour),
|
||||
("IPV4_EGRESS_DST_1HOUR", "IPV6_EGRESS_DST_1HOUR"),
|
||||
),
|
||||
];
|
||||
|
||||
pub fn new(
|
||||
app_config: Arc<AppConfig>,
|
||||
ingress_ebpf: &mut Ebpf,
|
||||
egress_ebpf: &mut Ebpf,
|
||||
) -> Result<Statistics, Error> {
|
||||
let boot_time = boot_time();
|
||||
let mut ipv4_maps = HashMap::new();
|
||||
let mut ipv6_maps = HashMap::new();
|
||||
let geo_ip = match GeoIpService::new(&app_config.geoip_db_name) {
|
||||
Ok(service) => Some(Arc::new(service)),
|
||||
Err(err) => {
|
||||
log!(MiscError::InvalidGeoIPConfiguration(err));
|
||||
None
|
||||
}
|
||||
};
|
||||
for (key, (ipv4_name, ipv6_name)) in Self::INGRESS_MAPS {
|
||||
ipv4_maps.insert(key, RwLock::new(FlowMap::new(ingress_ebpf, ipv4_name)?));
|
||||
ipv6_maps.insert(key, RwLock::new(FlowMap::new(ingress_ebpf, ipv6_name)?));
|
||||
}
|
||||
for (key, (ipv4_name, ipv6_name)) in Self::EGRESS_MAPS {
|
||||
ipv4_maps.insert(key, RwLock::new(FlowMap::new(egress_ebpf, ipv4_name)?));
|
||||
ipv6_maps.insert(key, RwLock::new(FlowMap::new(egress_ebpf, ipv6_name)?));
|
||||
}
|
||||
let statistics = Statistics {
|
||||
app_config,
|
||||
boot_time,
|
||||
geo_ip,
|
||||
ipv4_maps,
|
||||
ipv6_maps,
|
||||
};
|
||||
Ok(statistics)
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>) -> oneshot::Sender<()> {
|
||||
let refresh_interval = self.app_config.refresh_interval;
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
let mut receiver = receiver;
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = &mut receiver => break,
|
||||
_ = sleep(Duration::from_secs(refresh_interval)) => {
|
||||
self.cleanup_expired_flows().await;
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
sender
|
||||
}
|
||||
|
||||
pub async fn cleanup_expired_flows(self: &Arc<Self>) {
|
||||
let boot_time = self.boot_time;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
for ((_, _, time_type), map) in self.ipv4_maps.iter() {
|
||||
map.write().await.cleanup(boot_time, now, time_type.duration())
|
||||
}
|
||||
for ((_, _, time_type), map) in self.ipv6_maps.iter() {
|
||||
map.write().await.cleanup(boot_time, now, time_type.duration())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_ipv4_flow_data(
|
||||
&self,
|
||||
direction: Direction,
|
||||
flow_direction: FlowDirection,
|
||||
time_type: TimeType,
|
||||
) -> HashMap<SocketAddrV4, FlowStatsWithGeo> {
|
||||
let flow_data = self
|
||||
.ipv4_maps
|
||||
.get(&(direction, flow_direction, time_type))
|
||||
.unwrap()
|
||||
.write()
|
||||
.await
|
||||
.get_map();
|
||||
|
||||
if let Some(ref geo_ip) = self.geo_ip {
|
||||
let futures: Vec<_> = flow_data
|
||||
.into_iter()
|
||||
.map(|(addr, stats)| {
|
||||
let geo_ip = geo_ip.clone();
|
||||
async move {
|
||||
let ip = IpAddr::V4(*addr.ip());
|
||||
let geo = geo_ip.lookup(ip).await.ok().flatten();
|
||||
(addr, FlowStatsWithGeo { stats, geo })
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
join_all(futures).await.into_iter().collect()
|
||||
} else {
|
||||
flow_data
|
||||
.into_iter()
|
||||
.map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None }))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_ipv6_flow_data(
|
||||
&self,
|
||||
direction: Direction,
|
||||
flow_direction: FlowDirection,
|
||||
time_type: TimeType,
|
||||
) -> HashMap<SocketAddrV6, FlowStatsWithGeo> {
|
||||
let flow_data = self
|
||||
.ipv6_maps
|
||||
.get(&(direction, flow_direction, time_type))
|
||||
.unwrap()
|
||||
.write()
|
||||
.await
|
||||
.get_map();
|
||||
|
||||
if let Some(ref geo_ip) = self.geo_ip {
|
||||
let futures: Vec<_> = flow_data
|
||||
.into_iter()
|
||||
.map(|(addr, stats)| {
|
||||
let geo_ip = geo_ip.clone();
|
||||
async move {
|
||||
let ip = IpAddr::V6(*addr.ip());
|
||||
let geo = geo_ip.lookup(ip).await.ok().flatten();
|
||||
(addr, FlowStatsWithGeo { stats, geo })
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
join_all(futures).await.into_iter().collect()
|
||||
} else {
|
||||
flow_data
|
||||
.into_iter()
|
||||
.map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None }))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FlowMap<T> {
|
||||
map: AyaHashMap<MapData, T, FlowStats>,
|
||||
}
|
||||
|
||||
impl<T: NativeConvert + Pod> FlowMap<T> {
|
||||
fn new(ebpf: &mut Ebpf, map_name: &str) -> Result<Self, Error> {
|
||||
let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?;
|
||||
let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(Self { map })
|
||||
}
|
||||
|
||||
fn get_map(&self) -> HashMap<T::Native, FlowStats> {
|
||||
self.map
|
||||
.iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(|(key, value)| (key.into_native(), FlowStats::from(value)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn cleanup(&mut self, boot_time: u64, now: u64, window: u64) {
|
||||
let expired_keys: Vec<T> = self
|
||||
.map
|
||||
.iter()
|
||||
.filter_map(|result| {
|
||||
result
|
||||
.ok()
|
||||
.and_then(|(key, stats)| (now - stats.last_seen - boot_time > window).then_some(key))
|
||||
})
|
||||
.collect();
|
||||
expired_keys.iter().for_each(|key| {
|
||||
let _ = self.map.remove(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
368
net-guardia/src/core/ebpf/xsk_manager.rs
Normal file
368
net-guardia/src/core/ebpf/xsk_manager.rs
Normal file
@ -0,0 +1,368 @@
|
||||
use std::ffi::CString;
|
||||
use std::io::Write;
|
||||
use std::num::NonZero;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use aya::maps::{MapData, XskMap};
|
||||
use aya::Ebpf;
|
||||
use crossbeam::channel::{bounded, Receiver, Sender};
|
||||
use crossbeam::queue::SegQueue;
|
||||
use macros::log;
|
||||
use parking_lot::Mutex;
|
||||
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::infrastructure::app_config::AppConfig;
|
||||
use crate::ml::engine::{Engine, PacketProcessor};
|
||||
use crate::model::config::Config;
|
||||
use crate::model::direction::Direction;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::ebpf::EbpfLog;
|
||||
|
||||
pub struct XskManager {
|
||||
app_config: Arc<AppConfig>,
|
||||
xsk_map: Mutex<XskMap<MapData>>,
|
||||
egress_xsk_map: Mutex<XskMap<MapData>>,
|
||||
}
|
||||
|
||||
impl XskManager {
|
||||
pub fn new(app_config: Arc<AppConfig>, ingress_ebpf: &mut Ebpf, egress_ebpf: &mut Ebpf) -> Result<Self, Error> {
|
||||
let map = ingress_ebpf
|
||||
.take_map("INGRESS_XSKS_MAP")
|
||||
.ok_or(EbpfError::MapNotFound)?;
|
||||
let xsk_map = XskMap::try_from(map).map_err(EbpfError::MapOperationError)?;
|
||||
|
||||
let egress_map = egress_ebpf.take_map("EGRESS_XSKS_MAP").ok_or(EbpfError::MapNotFound)?;
|
||||
let egress_xsk_map = XskMap::try_from(egress_map).map_err(EbpfError::MapOperationError)?;
|
||||
|
||||
Ok(Self {
|
||||
app_config,
|
||||
xsk_map: Mutex::new(xsk_map),
|
||||
egress_xsk_map: Mutex::new(egress_xsk_map),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&self, ml_engine: Option<Arc<Engine>>, shutdowns: &SegQueue<oneshot::Sender<()>>) -> Result<(), Error> {
|
||||
let config = self.app_config.config.clone();
|
||||
let combined_queue_count = config.combined_queue_count;
|
||||
|
||||
let packet_processor = ml_engine.map(|engine| Arc::new(PacketProcessor::new(engine)));
|
||||
|
||||
for queue_id in 0..combined_queue_count {
|
||||
let (ingress_to_egress_tx, ingress_to_egress_rx) = bounded(config.channel_size);
|
||||
let (egress_to_ingress_tx, egress_to_ingress_rx) = bounded(config.channel_size);
|
||||
|
||||
let ingress_xsk = XskPair::new(
|
||||
config.clone(),
|
||||
queue_id,
|
||||
&config.ingress_ifname,
|
||||
&config.egress_ifname,
|
||||
Direction::Ingress,
|
||||
packet_processor.clone(),
|
||||
)?;
|
||||
|
||||
let egress_xsk = XskPair::new(
|
||||
config.clone(),
|
||||
queue_id,
|
||||
&config.egress_ifname,
|
||||
&config.ingress_ifname,
|
||||
Direction::Egress,
|
||||
packet_processor.clone(),
|
||||
)?;
|
||||
|
||||
let mut xsk_map = self.xsk_map.lock();
|
||||
let ingress_fd = ingress_xsk.rx.fd().as_raw_fd();
|
||||
xsk_map
|
||||
.set(queue_id, ingress_fd, 0)
|
||||
.map_err(EbpfError::AfXdpSetFailed)?;
|
||||
drop(xsk_map);
|
||||
|
||||
let mut egress_xsk_map = self.egress_xsk_map.lock();
|
||||
let egress_fd = egress_xsk.rx.fd().as_raw_fd();
|
||||
egress_xsk_map
|
||||
.set(queue_id, egress_fd, 0)
|
||||
.map_err(EbpfError::AfXdpSetFailed)?;
|
||||
drop(egress_xsk_map);
|
||||
|
||||
let ingress_shutdown = ingress_xsk.run(ingress_to_egress_tx, egress_to_ingress_rx)?;
|
||||
shutdowns.push(ingress_shutdown);
|
||||
|
||||
let egress_shutdown = egress_xsk.run(egress_to_ingress_tx, ingress_to_egress_rx)?;
|
||||
shutdowns.push(egress_shutdown);
|
||||
|
||||
log!(EbpfLog::QueuePairStarted(queue_id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct XskPair {
|
||||
direction: Direction,
|
||||
umem: Arc<Umem>,
|
||||
fill_queue: FillQueue,
|
||||
comp_queue: CompQueue,
|
||||
tx: TxQueue,
|
||||
rx: RxQueue,
|
||||
frame_pool: Arc<Mutex<Vec<FrameDesc>>>, // SegQueue
|
||||
packet_processor: Option<Arc<PacketProcessor>>,
|
||||
}
|
||||
|
||||
impl XskPair {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
queue_id: u32,
|
||||
rx_ifname: &str,
|
||||
tx_ifname: &str,
|
||||
direction: Direction,
|
||||
packet_processor: Option<Arc<PacketProcessor>>,
|
||||
) -> Result<Self, Error> {
|
||||
let rx_ifname_c = CString::new(rx_ifname).map_err(|_| SystemError::UnknownError)?;
|
||||
|
||||
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)?;
|
||||
let tx_queue_size = QueueSize::new(config.tx_queue_size).map_err(|_| SystemError::InvalidConfig)?;
|
||||
let rx_queue_size = QueueSize::new(config.rx_queue_size).map_err(|_| SystemError::InvalidConfig)?;
|
||||
let frame_size = FrameSize::new(config.frame_size).map_err(|_| SystemError::InvalidConfig)?;
|
||||
let frame_count = NonZero::new(config.frame_count).ok_or(SystemError::InvalidConfig)?;
|
||||
|
||||
let umem_config = UmemConfig::builder()
|
||||
.fill_queue_size(fill_queue_size)
|
||||
.comp_queue_size(comp_queue_size)
|
||||
.frame_size(frame_size)
|
||||
.frame_headroom(0)
|
||||
.build()
|
||||
.map_err(EbpfError::UmemSetFailed)?;
|
||||
|
||||
let (umem, frame_descs) = Umem::new(umem_config, frame_count, false).map_err(EbpfError::UmemSetFailed)?;
|
||||
|
||||
let socket_config = SocketConfig::builder()
|
||||
.tx_queue_size(tx_queue_size)
|
||||
.rx_queue_size(rx_queue_size)
|
||||
.bind_flags(BindFlags::XDP_ZEROCOPY)
|
||||
.libxdp_flags(LibxdpFlags::XSK_LIBXDP_FLAGS_INHIBIT_PROG_LOAD)
|
||||
.build();
|
||||
|
||||
let interface = Interface::new(rx_ifname_c);
|
||||
|
||||
let (tx, rx, queue) =
|
||||
unsafe { Socket::new(socket_config, &umem, &interface, queue_id).map_err(EbpfError::SocketSetFailed)? };
|
||||
|
||||
let (mut fill_queue, comp_queue) = queue.ok_or(EbpfError::UnknownError)?;
|
||||
|
||||
let total_frames = frame_descs.len();
|
||||
let fill_frames_count = (total_frames / 2).min(config.fill_queue_size as usize);
|
||||
|
||||
let fill_frames: Vec<FrameDesc> = frame_descs.iter().take(fill_frames_count).copied().collect();
|
||||
|
||||
let submitted = unsafe { fill_queue.produce(&fill_frames) };
|
||||
if submitted != fill_frames.len() {
|
||||
log!(EbpfLog::QueueInitIncomplete);
|
||||
}
|
||||
|
||||
let pool_frames: Vec<FrameDesc> = frame_descs.iter().skip(fill_frames_count).copied().collect();
|
||||
|
||||
let xsk_pair = Self {
|
||||
direction,
|
||||
umem: Arc::new(umem),
|
||||
fill_queue,
|
||||
comp_queue,
|
||||
tx,
|
||||
rx,
|
||||
frame_pool: Arc::new(Mutex::new(pool_frames)),
|
||||
packet_processor,
|
||||
};
|
||||
|
||||
Ok(xsk_pair)
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
mut self,
|
||||
forward_tx: Sender<Vec<u8>>,
|
||||
forward_rx: Receiver<Vec<u8>>,
|
||||
) -> Result<oneshot::Sender<()>, EbpfError> {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
|
||||
let thread_name = format!("xsk-{:?}", self.direction);
|
||||
|
||||
thread::Builder::new()
|
||||
.name(thread_name.clone())
|
||||
.spawn(move || {
|
||||
let mut shutdown_rx = Some(shutdown_rx);
|
||||
let mut idle_count: u32 = 0;
|
||||
|
||||
loop {
|
||||
if let Some(ref mut rx) = shutdown_rx {
|
||||
match rx.try_recv() {
|
||||
Ok(_) | Err(oneshot::error::TryRecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
Err(oneshot::error::TryRecvError::Empty) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut total_activity = 0;
|
||||
|
||||
match self.process_comp_queue() {
|
||||
Ok(count) => total_activity += count,
|
||||
Err(e) => log!(EbpfLog::CompQueueError(format!("{:?}", e))),
|
||||
}
|
||||
|
||||
match self.process_rx_queue(&forward_tx) {
|
||||
Ok(count) => total_activity += count,
|
||||
Err(e) => log!(EbpfLog::RXQueueError(format!("{:?}", e))),
|
||||
}
|
||||
|
||||
match self.process_tx_queue(&forward_rx) {
|
||||
Ok(count) => total_activity += count,
|
||||
Err(e) => log!(EbpfLog::TXQueueError(format!("{:?}", e))),
|
||||
}
|
||||
|
||||
if total_activity == 0 {
|
||||
idle_count = idle_count.saturating_add(1);
|
||||
} else {
|
||||
idle_count = 0;
|
||||
}
|
||||
|
||||
let sleep_us = match idle_count {
|
||||
0..=10 => 1,
|
||||
11..=100 => 10,
|
||||
_ => 100,
|
||||
};
|
||||
|
||||
thread::sleep(Duration::from_micros(sleep_us));
|
||||
}
|
||||
|
||||
log!(EbpfLog::XSKShutdown);
|
||||
})
|
||||
.map(|_| shutdown_tx)
|
||||
.map_err(|e| {
|
||||
log!(EbpfLog::ThreadSpawnFailed(thread_name.clone(), e.to_string()));
|
||||
EbpfError::ThreadSpawnFailed(e)
|
||||
})
|
||||
}
|
||||
|
||||
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) };
|
||||
|
||||
if nb_completed > 0 {
|
||||
let mut pool = self.frame_pool.lock();
|
||||
|
||||
for desc in comp_descs.iter().take(nb_completed) {
|
||||
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];
|
||||
let rx_count = unsafe { self.rx.consume(&mut rx_descs) };
|
||||
|
||||
if rx_count > 0 {
|
||||
for rx_desc in rx_descs.iter().take(rx_count) {
|
||||
let lengths = rx_desc.lengths();
|
||||
let packet_len = lengths.data() as usize;
|
||||
|
||||
let data = unsafe { self.umem.data(rx_desc) };
|
||||
let packet_data = data.contents()[..packet_len].to_vec();
|
||||
|
||||
if let Some(ref processor) = self.packet_processor {
|
||||
processor.process(&packet_data, self.direction == Direction::Ingress);
|
||||
}
|
||||
|
||||
if let Err(e) = forward_tx.try_send(packet_data) {
|
||||
match e {
|
||||
crossbeam::channel::TrySendError::Full(_) => {
|
||||
log!(EbpfLog::ForwardChannelFull);
|
||||
}
|
||||
crossbeam::channel::TrySendError::Disconnected(_) => {
|
||||
log!(EbpfLog::ForwardChannelDisconnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let produced = self.fill_queue.produce(&rx_descs[..rx_count]);
|
||||
if produced != rx_count {
|
||||
log!(EbpfLog::FillQueueIncomplete(produced, rx_count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(rx_count)
|
||||
}
|
||||
|
||||
fn process_tx_queue(&mut self, forward_rx: &Receiver<Vec<u8>>) -> 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);
|
||||
if packets_to_send.len() >= 64 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if packets_to_send.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let _ = self.process_comp_queue();
|
||||
|
||||
let pool_size = {
|
||||
let pool = self.frame_pool.lock();
|
||||
pool.len()
|
||||
};
|
||||
|
||||
if pool_size == 0 {
|
||||
log!(EbpfLog::FramePoolExhausted(packets_to_send.len()));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if frames.is_empty() {
|
||||
log!(EbpfLog::NoFramesAvailable);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
for (frame, packet) in frames.iter_mut().zip(packets_to_send.iter()) {
|
||||
unsafe {
|
||||
self.umem
|
||||
.data_mut(frame)
|
||||
.cursor()
|
||||
.write_all(packet)
|
||||
.map_err(EbpfError::AfXdpSetFailed)?;
|
||||
}
|
||||
}
|
||||
|
||||
let nb_submitted = unsafe { self.tx.produce(&frames) };
|
||||
|
||||
if let Err(e) = self.tx.wakeup() {
|
||||
if e.kind() != std::io::ErrorKind::WouldBlock {
|
||||
log!(EbpfLog::TXWakeupFailed(e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nb_submitted)
|
||||
}
|
||||
}
|
||||
39
net-guardia/src/core/infrastructure/app_config.rs
Normal file
39
net-guardia/src/core/infrastructure/app_config.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use std::fs;
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::model::config::{Config, ConfigTable};
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
|
||||
pub struct AppConfig {
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let toml_string = fs::read_to_string("./config.toml").map_err(SystemError::ConfigNotFound)?;
|
||||
let config_table = toml::from_str::<ConfigTable>(&toml_string).map_err(|_| SystemError::InvalidConfig)?;
|
||||
let config = config_table.config;
|
||||
if !Self::validate(&config) {
|
||||
Err(SystemError::InvalidConfig)?
|
||||
} else {
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(config: &Config) -> bool {
|
||||
Self::validate_second(config.refresh_interval)
|
||||
}
|
||||
|
||||
fn validate_second(second: u64) -> bool {
|
||||
second <= 3600
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AppConfig {
|
||||
type Target = Config;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
112
net-guardia/src/core/infrastructure/geoip.rs
Normal file
112
net-guardia/src/core/infrastructure/geoip.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use std::net::IpAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use maxminddb::{geoip2, MaxMindDbError, Reader};
|
||||
use tokio::sync::RwLock;
|
||||
use lru::LruCache;
|
||||
use std::num::NonZeroUsize;
|
||||
use tokio::task;
|
||||
|
||||
use crate::model::geo_stats::GeoLocation;
|
||||
use crate::utils::ip_address;
|
||||
|
||||
pub struct GeoIpService {
|
||||
reader: Arc<Reader<Vec<u8>>>,
|
||||
cache: Arc<RwLock<LruCache<IpAddr, Option<GeoLocation>>>>,
|
||||
}
|
||||
|
||||
impl GeoIpService {
|
||||
pub fn new(db_name: &str) -> Result<Self, MaxMindDbError> {
|
||||
let db_path = PathBuf::from("net-guardia/static/geo").join(db_name);
|
||||
Self::with_cache_size(db_path, 10000)
|
||||
}
|
||||
|
||||
pub fn with_cache_size<P: AsRef<Path>>(
|
||||
db_path: P,
|
||||
cache_size: usize,
|
||||
) -> Result<Self, MaxMindDbError> {
|
||||
let reader = Reader::open_readfile(db_path)?;
|
||||
let cache_capacity = NonZeroUsize::new(cache_size)
|
||||
.unwrap_or_else(|| NonZeroUsize::new(10000).unwrap());
|
||||
|
||||
Ok(Self {
|
||||
reader: Arc::new(reader),
|
||||
cache: Arc::new(RwLock::new(LruCache::new(cache_capacity))),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn lookup(&self, ip: IpAddr) -> Result<Option<GeoLocation>, MaxMindDbError> {
|
||||
if ip_address::is_private_ip(&ip) {
|
||||
return Ok(Some(GeoLocation {
|
||||
country: Some("Local IP".into()),
|
||||
country_code: Some("Local".into()),
|
||||
city: None,
|
||||
latitude: None,
|
||||
longitude: None,
|
||||
timezone: None,
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
let cache = self.cache.read().await;
|
||||
if let Some(cached) = cache.peek(&ip) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let reader = self.reader.clone();
|
||||
let result = task::spawn_blocking(move || {
|
||||
Self::lookup_from_db_blocking(&reader, ip)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| MaxMindDbError::InvalidDatabase {
|
||||
message: format!("Task join error: {}", e),
|
||||
offset: None,
|
||||
})??;
|
||||
|
||||
{
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.put(ip, result.clone());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn lookup_from_db_blocking(
|
||||
reader: &Reader<Vec<u8>>,
|
||||
ip: IpAddr,
|
||||
) -> Result<Option<GeoLocation>, MaxMindDbError> {
|
||||
let lookup_result = reader.lookup(ip)?;
|
||||
let city_option: Option<geoip2::City> = lookup_result.decode()?;
|
||||
|
||||
Ok(city_option.map(|city| {
|
||||
let country_name = city.country.names.english
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let country_code = city.country.iso_code
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let city_name = city.city.names.english
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let latitude = city.location.latitude.or(Some(0.0));
|
||||
let longitude = city.location.longitude.or(Some(0.0));
|
||||
let timezone = city.location.time_zone.map(|s| s.to_string());
|
||||
|
||||
GeoLocation {
|
||||
country: country_name,
|
||||
country_code,
|
||||
city: city_name,
|
||||
latitude,
|
||||
longitude,
|
||||
timezone,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn cache_stats(&self) -> (usize, usize) {
|
||||
let cache = self.cache.read().await;
|
||||
(cache.len(), cache.cap().get())
|
||||
}
|
||||
}
|
||||
341
net-guardia/src/core/infrastructure/health.rs
Normal file
341
net-guardia/src/core/infrastructure/health.rs
Normal file
@ -0,0 +1,341 @@
|
||||
// net-guardia/src/core/ebpf/health.rs
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use sysinfo::{Components, Networks, System};
|
||||
use tokio::sync::{broadcast, oneshot, RwLock};
|
||||
use tokio::time::interval;
|
||||
use macros::log;
|
||||
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::model::log::health::Health;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::health::{
|
||||
ConfiguredNetworkStats,
|
||||
CpuCoreInfo,
|
||||
CpuDetails,
|
||||
LoadAverage,
|
||||
MemoryUsage,
|
||||
NetworkStats,
|
||||
SystemHealthMetrics,
|
||||
SystemHealthStatus,
|
||||
SystemInfo
|
||||
};
|
||||
|
||||
pub struct SystemHealth {
|
||||
system: RwLock<System>,
|
||||
networks: RwLock<Networks>,
|
||||
components: RwLock<Components>,
|
||||
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);
|
||||
|
||||
let health = SystemHealth {
|
||||
system: RwLock::new(System::new_all()),
|
||||
networks: RwLock::new(Networks::new_with_refreshed_list()),
|
||||
components: RwLock::new(Components::new_with_refreshed_list()),
|
||||
broadcast_tx,
|
||||
ingress_interface: config.ingress_ifname.clone(),
|
||||
egress_interface: config.egress_ifname.clone(),
|
||||
// management_interface: config.management_ifindex.clone(),
|
||||
};
|
||||
|
||||
Ok(health)
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>, monitoring_interval: Duration) -> oneshot::Sender<()> {
|
||||
let (sender, mut receiver) = oneshot::channel();
|
||||
let health = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = interval(monitoring_interval);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = &mut receiver => {
|
||||
break;
|
||||
}
|
||||
_ = interval_timer.tick() => {
|
||||
health.refresh_and_broadcast().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sender
|
||||
}
|
||||
|
||||
async fn refresh_and_broadcast(&self) {
|
||||
self.system.write().await.refresh_all();
|
||||
self.networks.write().await.refresh(true);
|
||||
self.components.write().await.refresh(true);
|
||||
|
||||
let system = self.system.read().await;
|
||||
let networks = self.networks.read().await;
|
||||
let components = self.components.read().await;
|
||||
|
||||
let metrics = Self::collect_metrics(
|
||||
&system,
|
||||
&networks,
|
||||
&components,
|
||||
&self.ingress_interface,
|
||||
&self.egress_interface,
|
||||
// &self.management_interface,
|
||||
);
|
||||
|
||||
drop(system);
|
||||
drop(networks);
|
||||
drop(components);
|
||||
|
||||
if self.broadcast_tx.receiver_count() > 0 {
|
||||
if let Err(e) = self.broadcast_tx.send(metrics) {
|
||||
log!(Health::BroadcastFailed(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_metrics(
|
||||
system: &System,
|
||||
networks: &Networks,
|
||||
components: &Components,
|
||||
ingress_interface: &str,
|
||||
egress_interface: &str,
|
||||
// management_interface: &str,
|
||||
) -> SystemHealthMetrics {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let boot_time = System::boot_time();
|
||||
let uptime_seconds = timestamp - boot_time;
|
||||
|
||||
let system_info = Self::collect_system_info(system);
|
||||
let cpu_details = Self::collect_cpu_details(system);
|
||||
|
||||
let memory_usage = MemoryUsage {
|
||||
total: system.total_memory(),
|
||||
used: system.used_memory(),
|
||||
available: system.available_memory(),
|
||||
usage_percent: (system.used_memory() as f32 / system.total_memory() as f32) * 100.0,
|
||||
swap_total: system.total_swap(),
|
||||
swap_used: system.used_swap(),
|
||||
};
|
||||
|
||||
let network_stats = Self::collect_configured_network_stats(
|
||||
networks,
|
||||
ingress_interface,
|
||||
egress_interface,
|
||||
// management_interface,
|
||||
);
|
||||
|
||||
let load_average = System::load_average();
|
||||
let load_average = if load_average.one != 0.0 || load_average.five != 0.0 || load_average.fifteen != 0.0 {
|
||||
Some(LoadAverage {
|
||||
one_minute: load_average.one,
|
||||
five_minute: load_average.five,
|
||||
fifteen_minute: load_average.fifteen,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let temperature = components
|
||||
.iter()
|
||||
.find(|component| {
|
||||
let label = component.label().to_lowercase();
|
||||
label.contains("cpu") || label.contains("core") || label.contains("processor")
|
||||
})
|
||||
.and_then(|component| component.temperature());
|
||||
|
||||
SystemHealthMetrics {
|
||||
timestamp,
|
||||
boot_time,
|
||||
uptime_seconds,
|
||||
system_info,
|
||||
cpu_details,
|
||||
memory_usage,
|
||||
network_stats,
|
||||
load_average,
|
||||
temperature,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_system_info(system: &System) -> SystemInfo {
|
||||
SystemInfo {
|
||||
kernel_version: System::kernel_version(),
|
||||
os_name: System::name(),
|
||||
os_version: System::os_version(),
|
||||
architecture: std::env::consts::ARCH.to_string(),
|
||||
total_processes: system.processes().len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_cpu_details(system: &System) -> CpuDetails {
|
||||
let cpus = system.cpus();
|
||||
|
||||
let cpu_usage = cpus.iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / cpus.len() as f32;
|
||||
|
||||
let cores: Vec<CpuCoreInfo> = cpus
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, cpu)| CpuCoreInfo {
|
||||
core_id: index,
|
||||
usage_percent: cpu.cpu_usage(),
|
||||
frequency: cpu.frequency(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let cpu_brand = cpus
|
||||
.first()
|
||||
.map(|cpu| cpu.brand().to_string())
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
let avg_frequency = if !cores.is_empty() {
|
||||
cores.iter().map(|core| core.frequency).sum::<u64>() / cores.len() as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
CpuDetails {
|
||||
cpu_brand,
|
||||
core_count: cores.len(),
|
||||
cpu_usage,
|
||||
cpu_frequency: avg_frequency,
|
||||
cores,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_configured_network_stats(
|
||||
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 {
|
||||
interface: interface_name.to_string(),
|
||||
bytes_received: network.total_received(),
|
||||
bytes_transmitted: network.total_transmitted(),
|
||||
packets_received: network.total_packets_received(),
|
||||
packets_transmitted: network.total_packets_transmitted(),
|
||||
errors_received: network.total_errors_on_received(),
|
||||
errors_transmitted: network.total_errors_on_transmitted(),
|
||||
})
|
||||
};
|
||||
|
||||
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()));
|
||||
}
|
||||
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 {
|
||||
self.system.write().await.refresh_all();
|
||||
self.networks.write().await.refresh(true);
|
||||
self.components.write().await.refresh(true);
|
||||
|
||||
let system = self.system.read().await;
|
||||
let networks = self.networks.read().await;
|
||||
let components = self.components.read().await;
|
||||
|
||||
Self::collect_metrics(
|
||||
&system,
|
||||
&networks,
|
||||
&components,
|
||||
&self.ingress_interface,
|
||||
&self.egress_interface,
|
||||
// &self.management_interface,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn subscribe_to_metrics(&self) -> broadcast::Receiver<SystemHealthMetrics> {
|
||||
self.broadcast_tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn is_system_healthy(&self) -> SystemHealthStatus {
|
||||
let metrics = self.get_current_metrics().await;
|
||||
|
||||
let mut status = SystemHealthStatus {
|
||||
overall_healthy: true,
|
||||
issues: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
|
||||
if metrics.cpu_details.cpu_usage > 90.0 {
|
||||
status.overall_healthy = false;
|
||||
status
|
||||
.issues
|
||||
.push(format!("High CPU usage: {:.1}%", metrics.cpu_details.cpu_usage));
|
||||
} else if metrics.cpu_details.cpu_usage > 75.0 {
|
||||
status
|
||||
.warnings
|
||||
.push(format!("Moderate CPU usage: {:.1}%", metrics.cpu_details.cpu_usage));
|
||||
}
|
||||
|
||||
if metrics.memory_usage.usage_percent > 95.0 {
|
||||
status.overall_healthy = false;
|
||||
status.issues.push(format!(
|
||||
"Critical memory usage: {:.1}%",
|
||||
metrics.memory_usage.usage_percent
|
||||
));
|
||||
} else if metrics.memory_usage.usage_percent > 80.0 {
|
||||
status.warnings.push(format!(
|
||||
"High memory usage: {:.1}%",
|
||||
metrics.memory_usage.usage_percent
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(temp) = metrics.temperature {
|
||||
if temp > 80.0 {
|
||||
status.overall_healthy = false;
|
||||
status
|
||||
.issues
|
||||
.push(format!("High CPU temperature: {:.1}°C", temp));
|
||||
} else if temp > 70.0 {
|
||||
status
|
||||
.warnings
|
||||
.push(format!("Elevated CPU temperature: {:.1}°C", temp));
|
||||
}
|
||||
}
|
||||
|
||||
if metrics.network_stats.ingress.is_none() {
|
||||
status.overall_healthy = false;
|
||||
status.issues.push("Ingress interface not available".to_string());
|
||||
}
|
||||
if metrics.network_stats.egress.is_none() {
|
||||
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
|
||||
}
|
||||
}
|
||||
80
net-guardia/src/core/infrastructure/ml_alert.rs
Normal file
80
net-guardia/src/core/infrastructure/ml_alert.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::error;
|
||||
|
||||
use crate::model::ml_detection::DetectionResult;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AlertMessage {
|
||||
pub timestamp: u64,
|
||||
pub flow_key: String,
|
||||
pub src_ip: String,
|
||||
pub dst_ip: String,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub protocol: u8,
|
||||
pub is_attack: bool,
|
||||
pub attack_type: Option<String>,
|
||||
pub confidence: f32,
|
||||
pub ae_score: f32,
|
||||
}
|
||||
|
||||
impl AlertMessage {
|
||||
pub fn from_detection_result(result: &DetectionResult) -> Self {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
Self {
|
||||
timestamp,
|
||||
flow_key: result.flow_key.clone(),
|
||||
src_ip: result.flow_key_raw.src_ip.clone(),
|
||||
dst_ip: result.flow_key_raw.dst_ip.clone(),
|
||||
src_port: result.flow_key_raw.src_port,
|
||||
dst_port: result.flow_key_raw.dst_port,
|
||||
protocol: result.flow_key_raw.protocol,
|
||||
is_attack: result.is_attack,
|
||||
attack_type: result.attack_type.clone(),
|
||||
confidence: result.confidence,
|
||||
ae_score: result.ae_score,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MLAlert {
|
||||
broadcast_tx: broadcast::Sender<AlertMessage>,
|
||||
}
|
||||
|
||||
impl MLAlert {
|
||||
pub fn new() -> Self {
|
||||
let (broadcast_tx, _) = broadcast::channel(100);
|
||||
|
||||
MLAlert {
|
||||
broadcast_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_to_alerts(&self) -> broadcast::Receiver<AlertMessage> {
|
||||
self.broadcast_tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn broadcast_alert(&self, result: &DetectionResult) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_subscribers(&self) -> bool {
|
||||
self.broadcast_tx.receiver_count() > 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MLAlert {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
95
net-guardia/src/core/infrastructure/mod.rs
Normal file
95
net-guardia/src/core/infrastructure/mod.rs
Normal file
@ -0,0 +1,95 @@
|
||||
pub mod app_config;
|
||||
pub mod health;
|
||||
pub mod geoip;
|
||||
pub mod ml_alert;
|
||||
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossbeam::queue::SegQueue;
|
||||
use macros::log;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::infrastructure::health::SystemHealth;
|
||||
use crate::core::infrastructure::ml_alert::MLAlert;
|
||||
use crate::ml::config_loader::InferenceConfig;
|
||||
use crate::ml::engine::Engine;
|
||||
use crate::ml::feature_extractor::FlowFeatures;
|
||||
use crate::ml::model_loader::MLModels;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::system::SystemLog;
|
||||
use crate::ml::traffic_logger::TrafficLogger;
|
||||
|
||||
pub struct AppServices {
|
||||
pub health: Arc<SystemHealth>,
|
||||
pub ml_alert: Arc<MLAlert>,
|
||||
pub ml_models: Arc<MLModels>,
|
||||
pub ml_engine: Arc<Engine>,
|
||||
shutdowns: SegQueue<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl AppServices {
|
||||
pub fn new(app_config: Arc<AppConfig>, inference_config: Arc<InferenceConfig>) -> Result<Self, Error> {
|
||||
let health = SystemHealth::new(app_config.clone())?;
|
||||
|
||||
let ml_models = Arc::new(MLModels::load_models(&app_config, &inference_config)?);
|
||||
let ml_alert = Arc::new(MLAlert::new());
|
||||
|
||||
let traffic_logger = if app_config.traffic_logging_mode {
|
||||
let csv_path = app_config.traffic_log_csv_path.clone();
|
||||
let mut header = FlowFeatures::all_feature_names_owned();
|
||||
header.push("Label".to_string());
|
||||
let logger = TrafficLogger::new(&csv_path, header)
|
||||
.map_err(|e| MiscError::TrafficLogCreateError(csv_path.clone(), e.to_string()))?;
|
||||
log!(SystemLog::TrafficLoggingEnabled(csv_path));
|
||||
Some(Arc::new(logger))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ml_engine = Arc::new(Engine::new(
|
||||
ml_models.clone(),
|
||||
inference_config.clone(),
|
||||
ml_alert.clone(),
|
||||
app_config.max_concurrent_flows,
|
||||
app_config.min_packets_for_inference,
|
||||
app_config.inference_batch_size,
|
||||
app_config.inference_interval_secs,
|
||||
app_config.aggregator_window_secs,
|
||||
traffic_logger,
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
health: Arc::new(health),
|
||||
ml_alert,
|
||||
ml_models,
|
||||
ml_engine,
|
||||
shutdowns: SegQueue::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<(), Error> {
|
||||
let health = self.health.clone();
|
||||
let ml_engine = self.ml_engine.clone();
|
||||
|
||||
let health_shutdown = health.run(Duration::from_secs(3)).await;
|
||||
self.shutdowns.push(health_shutdown);
|
||||
|
||||
let ml_shutdown = ml_engine.run().await;
|
||||
self.shutdowns.push(ml_shutdown);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn terminate(&self) {
|
||||
while let Some(shutdown) = self.shutdowns.pop() {
|
||||
if shutdown.send(()).is_err() {
|
||||
log!(SystemError::ShutdownSignalFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
net-guardia/src/core/mod.rs
Normal file
3
net-guardia/src/core/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod ebpf;
|
||||
pub mod infrastructure;
|
||||
pub mod system;
|
||||
249
net-guardia/src/core/system.rs
Normal file
249
net-guardia/src/core/system.rs
Normal file
@ -0,0 +1,249 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::route;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use aya::maps::{MapData, ProgramArray};
|
||||
use aya::programs::{Xdp, XdpFlags};
|
||||
use aya::Ebpf;
|
||||
use aya_log::EbpfLogger;
|
||||
use common::define::program_array::*;
|
||||
use macros::log;
|
||||
|
||||
use crate::core::ebpf::EbpfServices;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::infrastructure::AppServices;
|
||||
use crate::ml::config_loader::InferenceConfig;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::log::system::SystemLog;
|
||||
use crate::utils::logging::Logging;
|
||||
use crate::web::api::{control, default, health, misc, ml_alert};
|
||||
|
||||
pub struct System {
|
||||
pub app_config: Arc<AppConfig>,
|
||||
pub inference_config: Arc<InferenceConfig>,
|
||||
pub ebpf_services: Arc<EbpfServices>,
|
||||
pub app_services: Arc<AppServices>,
|
||||
pub ingress_ebpf: Ebpf,
|
||||
pub egress_ebpf: Ebpf,
|
||||
#[allow(dead_code)]
|
||||
ingress_program_array: ProgramArray<MapData>,
|
||||
#[allow(dead_code)]
|
||||
egress_program_array: ProgramArray<MapData>,
|
||||
}
|
||||
|
||||
impl System {
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
let (mut ingress_ebpf, ingress_program_array) = System::get_ingress_ebpf()?;
|
||||
let (mut egress_ebpf, egress_program_array) = System::get_egress_ebpf()?;
|
||||
let app_config = Arc::new(AppConfig::new()?);
|
||||
|
||||
let inference_config = Arc::new(InferenceConfig::load_file(&app_config.models_config_name)?);
|
||||
|
||||
let ebpf_services = Arc::new(EbpfServices::new(
|
||||
app_config.clone(),
|
||||
&mut ingress_ebpf,
|
||||
&mut egress_ebpf,
|
||||
)?);
|
||||
|
||||
let app_services = Arc::new(AppServices::new(app_config.clone(), inference_config.clone())?);
|
||||
|
||||
let system = System {
|
||||
app_config,
|
||||
inference_config,
|
||||
ebpf_services,
|
||||
app_services,
|
||||
ingress_ebpf,
|
||||
egress_ebpf,
|
||||
ingress_program_array,
|
||||
egress_program_array,
|
||||
};
|
||||
Ok(system)
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), Error> {
|
||||
let ebpf_services = self.ebpf_services.clone();
|
||||
let app_services = self.app_services.clone();
|
||||
Logging::initialize()?;
|
||||
log!(SystemLog::Initializing);
|
||||
|
||||
log!(MLLog::ModelsLoaded(
|
||||
self.app_services.ml_models.get_model_info("deep_autoencoder")
|
||||
));
|
||||
log!(MLLog::ModelsLoaded(
|
||||
self.app_services.ml_models.get_model_info("classifier")
|
||||
));
|
||||
|
||||
log!(MLLog::ConfigLoaded {
|
||||
features: self.inference_config.num_ae_features(),
|
||||
attacks: self.inference_config.num_attack_types()
|
||||
});
|
||||
|
||||
self.aya_log_init()?;
|
||||
log!(SystemLog::InitializeComplete);
|
||||
self.attach_ebpf()?;
|
||||
|
||||
ebpf_services.run(app_services.ml_engine.clone()).await?;
|
||||
app_services.run().await?;
|
||||
self.run_http_server().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn terminate(&self) -> Result<(), Error> {
|
||||
let ebpf_services = self.ebpf_services.clone();
|
||||
let app_services = self.app_services.clone();
|
||||
log!(SystemLog::Terminating);
|
||||
|
||||
ebpf_services.terminate();
|
||||
app_services.terminate();
|
||||
log!(SystemLog::TerminateComplete);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn aya_log_init(&mut self) -> Result<(), Error> {
|
||||
EbpfLogger::init(&mut self.ingress_ebpf).map_err(EbpfError::LoggerInitFailed)?;
|
||||
EbpfLogger::init(&mut self.egress_ebpf).map_err(EbpfError::LoggerInitFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attach_ebpf(&mut self) -> Result<(), Error> {
|
||||
let config = self.app_config.config.clone();
|
||||
let ingress_ifname = config.ingress_ifname;
|
||||
let egress_ifname = config.egress_ifname;
|
||||
Self::set_memory_limit()?;
|
||||
let ingress_xdp: &mut Xdp = self
|
||||
.ingress_ebpf
|
||||
.program_mut("net_guardia")
|
||||
.ok_or(EbpfError::ProgramNotFound)?
|
||||
.try_into()
|
||||
.map_err(EbpfError::GetProgramFailed)?;
|
||||
let egress_xdp: &mut Xdp = self
|
||||
.egress_ebpf
|
||||
.program_mut("net_guardia")
|
||||
.ok_or(EbpfError::ProgramNotFound)?
|
||||
.try_into()
|
||||
.map_err(EbpfError::GetProgramFailed)?;
|
||||
ingress_xdp.load().map_err(EbpfError::LoadProgramFailed)?;
|
||||
ingress_xdp
|
||||
.attach(&ingress_ifname, XdpFlags::DRV_MODE)
|
||||
.map_err(EbpfError::AttachProgramFailed)?;
|
||||
egress_xdp.load().map_err(EbpfError::LoadProgramFailed)?;
|
||||
egress_xdp
|
||||
.attach(&egress_ifname, XdpFlags::DRV_MODE)
|
||||
.map_err(EbpfError::AttachProgramFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_http_server(&self) -> Result<(), Error> {
|
||||
let app_config = self.app_config.clone();
|
||||
let inference_config = self.inference_config.clone();
|
||||
let access_control = self.ebpf_services.access_control.clone();
|
||||
let service = self.ebpf_services.service.clone();
|
||||
let statistics = self.ebpf_services.statistics.clone();
|
||||
let health = self.app_services.health.clone();
|
||||
let ml_alert = self.app_services.ml_alert.clone();
|
||||
let port = self.app_config.http_server_bind_port;
|
||||
HttpServer::new(move || {
|
||||
let cors = actix_cors::Cors::default()
|
||||
.allow_any_origin()
|
||||
.allow_any_method()
|
||||
.allow_any_header()
|
||||
.max_age(3600);
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.app_data(web::Data::from(app_config.clone()))
|
||||
.app_data(web::Data::from(inference_config.clone()))
|
||||
.app_data(web::Data::from(access_control.clone()))
|
||||
.app_data(web::Data::from(service.clone()))
|
||||
.app_data(web::Data::from(statistics.clone()))
|
||||
.app_data(web::Data::from(health.clone()))
|
||||
.app_data(web::Data::from(ml_alert.clone()))
|
||||
.service(control::initialize())
|
||||
.service(ml_alert::initialize())
|
||||
.service(health::initialize())
|
||||
.service(misc::initialize())
|
||||
.default_service(route().to(default::default_route))
|
||||
})
|
||||
.bind(format!("0.0.0.0:{}", port))
|
||||
.map_err(HttpError::BindPortError)?
|
||||
.run()
|
||||
.await
|
||||
.map_err(HttpError::ServerPanic)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
|
||||
let mut ingress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/net-guardia-ingress"
|
||||
)))
|
||||
.map_err(EbpfError::EbpfNotFound)?;
|
||||
let program_array = ingress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?;
|
||||
let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?;
|
||||
Self::load_program(
|
||||
&mut ingress_ebpf,
|
||||
&mut program_array,
|
||||
"access_control",
|
||||
ingress::ACCESS_CONTROL,
|
||||
)?;
|
||||
Self::load_program(&mut ingress_ebpf, &mut program_array, "service", ingress::SERVICE)?;
|
||||
Self::load_program(&mut ingress_ebpf, &mut program_array, "statistics", ingress::STATISTICS)?;
|
||||
Self::load_program(
|
||||
&mut ingress_ebpf,
|
||||
&mut program_array,
|
||||
"transmission",
|
||||
ingress::TRANSMISSION,
|
||||
)?;
|
||||
Ok((ingress_ebpf, program_array))
|
||||
}
|
||||
|
||||
fn get_egress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
|
||||
let mut egress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/net-guardia-egress"
|
||||
)))
|
||||
.map_err(EbpfError::EbpfNotFound)?;
|
||||
let program_array = egress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?;
|
||||
let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?;
|
||||
Self::load_program(&mut egress_ebpf, &mut program_array, "statistics", egress::STATISTICS)?;
|
||||
Self::load_program(
|
||||
&mut egress_ebpf,
|
||||
&mut program_array,
|
||||
"transmission",
|
||||
egress::TRANSMISSION,
|
||||
)?;
|
||||
Ok((egress_ebpf, program_array))
|
||||
}
|
||||
|
||||
fn load_program(
|
||||
ebpf: &mut Ebpf,
|
||||
program_array: &mut ProgramArray<MapData>,
|
||||
function_name: &str,
|
||||
index: u32,
|
||||
) -> Result<(), Error> {
|
||||
let program: &mut Xdp = ebpf
|
||||
.program_mut(function_name)
|
||||
.ok_or(EbpfError::ProgramNotFound)?
|
||||
.try_into()
|
||||
.map_err(EbpfError::MapOperationError)?;
|
||||
program.load().map_err(EbpfError::AttachProgramFailed)?;
|
||||
let fd = program.fd().map_err(|_| EbpfError::UnknownError)?;
|
||||
program_array.set(index, fd, 0).map_err(EbpfError::MapOperationError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_memory_limit() -> Result<(), Error> {
|
||||
let rlim = libc::rlimit {
|
||||
rlim_cur: libc::RLIM_INFINITY,
|
||||
rlim_max: libc::RLIM_INFINITY,
|
||||
};
|
||||
let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) };
|
||||
if ret != 0 {
|
||||
Err(MiscError::RamLimitUnlockError(ret))?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
16
net-guardia/src/main.rs
Normal file
16
net-guardia/src/main.rs
Normal file
@ -0,0 +1,16 @@
|
||||
mod core;
|
||||
mod model;
|
||||
mod utils;
|
||||
mod web;
|
||||
mod ml;
|
||||
|
||||
use crate::core::system::System;
|
||||
use crate::model::error::Error;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let mut system = System::new().await?;
|
||||
system.run().await?;
|
||||
system.terminate().await?;
|
||||
Ok(())
|
||||
}
|
||||
51
net-guardia/src/ml/aggregator.rs
Normal file
51
net-guardia/src/ml/aggregator.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::model::ml_detection::FlowKey;
|
||||
|
||||
pub struct AttackAggregator {
|
||||
detections: HashMap<FlowKey, Vec<(Instant, f32)>>,
|
||||
window_duration: Duration,
|
||||
min_detections: usize,
|
||||
alert_threshold_multiplier: f32,
|
||||
}
|
||||
|
||||
impl AttackAggregator {
|
||||
pub fn new(window_secs: u64, min_detections: usize) -> Self {
|
||||
Self {
|
||||
detections: HashMap::new(),
|
||||
window_duration: Duration::from_secs(window_secs),
|
||||
min_detections,
|
||||
alert_threshold_multiplier: 1.2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_alert(&mut self, flow_key: &FlowKey, score: f32, threshold: f32) -> bool {
|
||||
let now = Instant::now();
|
||||
|
||||
let detections = self.detections.entry(flow_key.clone()).or_default();
|
||||
detections.retain(|(time, _)| now.duration_since(*time) < self.window_duration);
|
||||
detections.push((now, score));
|
||||
|
||||
if detections.len() >= self.min_detections {
|
||||
let avg_score: f32 =
|
||||
detections.iter().map(|(_, s)| s).sum::<f32>() / detections.len() as f32;
|
||||
|
||||
return avg_score > threshold * self.alert_threshold_multiplier;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self) {
|
||||
let now = Instant::now();
|
||||
self.detections.retain(|_, detections| {
|
||||
detections.retain(|(time, _)| now.duration_since(*time) < self.window_duration);
|
||||
!detections.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn tracked_flows(&self) -> usize {
|
||||
self.detections.len()
|
||||
}
|
||||
}
|
||||
47
net-guardia/src/ml/config_loader.rs
Normal file
47
net-guardia/src/ml/config_loader.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::error::ml::MLError;
|
||||
use crate::model::ml_detection::{AENormalization, ClipParams, PrecisionLevels};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InferenceConfig {
|
||||
pub ae_feature_names: Vec<String>,
|
||||
pub ae_clip_params: HashMap<String, ClipParams>,
|
||||
pub ae_scaler_mean: Vec<f64>,
|
||||
pub ae_scaler_std: Vec<f64>,
|
||||
pub ae_post_clip_min: f64,
|
||||
pub ae_post_clip_max: f64,
|
||||
pub ae_threshold: f32,
|
||||
pub classifier_feature_names: Vec<String>,
|
||||
pub attack_labels: HashMap<String, String>,
|
||||
}
|
||||
|
||||
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() })?;
|
||||
let config: InferenceConfig = serde_json::from_str(&content)
|
||||
.map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn num_ae_features(&self) -> usize {
|
||||
self.ae_feature_names.len()
|
||||
}
|
||||
|
||||
pub fn num_classifier_features(&self) -> usize {
|
||||
self.classifier_feature_names.len()
|
||||
}
|
||||
|
||||
pub fn num_attack_types(&self) -> usize {
|
||||
self.attack_labels.len()
|
||||
}
|
||||
|
||||
pub fn get_attack_label(&self, id: usize) -> Option<&String> {
|
||||
self.attack_labels.get(&id.to_string())
|
||||
}
|
||||
}
|
||||
198
net-guardia/src/ml/engine.rs
Normal file
198
net-guardia/src/ml/engine.rs
Normal file
@ -0,0 +1,198 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use macros::log;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::interval;
|
||||
|
||||
use super::aggregator::AttackAggregator;
|
||||
use super::config_loader::InferenceConfig;
|
||||
use super::feature_extractor::FlowFeatures;
|
||||
use super::flow_tracker::FlowTracker;
|
||||
use super::inference::Inference;
|
||||
use super::model_loader::MLModels;
|
||||
use super::traffic_logger::TrafficLogger;
|
||||
|
||||
use crate::core::infrastructure::ml_alert::MLAlert;
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::ml_detection::{EngineStats, InferenceStats};
|
||||
use crate::utils::packet_parser::parse_packet;
|
||||
|
||||
pub struct Engine {
|
||||
flow_tracker: Arc<FlowTracker>,
|
||||
inference_pipeline: Arc<Inference>,
|
||||
aggregator: Arc<Mutex<AttackAggregator>>,
|
||||
ml_alert: Arc<MLAlert>,
|
||||
min_packets: usize,
|
||||
batch_size: usize,
|
||||
inference_interval_secs: u64,
|
||||
traffic_logger: Option<Arc<TrafficLogger>>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(
|
||||
models: Arc<MLModels>,
|
||||
config: Arc<InferenceConfig>,
|
||||
ml_alert: Arc<MLAlert>,
|
||||
max_flows: usize,
|
||||
min_packets: usize,
|
||||
batch_size: usize,
|
||||
interval_secs: u64,
|
||||
window_secs: u64,
|
||||
traffic_logger: Option<Arc<TrafficLogger>>,
|
||||
) -> Self {
|
||||
let flow_tracker = Arc::new(FlowTracker::new(max_flows));
|
||||
let inference_pipeline = Arc::new(Inference::new(models, config));
|
||||
|
||||
let min_detections = ((window_secs / interval_secs) / 2).max(1) as usize;
|
||||
let aggregator = Arc::new(Mutex::new(AttackAggregator::new(window_secs, min_detections)));
|
||||
|
||||
Self {
|
||||
flow_tracker,
|
||||
inference_pipeline,
|
||||
aggregator,
|
||||
ml_alert,
|
||||
min_packets,
|
||||
batch_size,
|
||||
inference_interval_secs: interval_secs,
|
||||
traffic_logger,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self: Arc<Self>) -> oneshot::Sender<()> {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
self.run_inference_loop(shutdown_rx).await;
|
||||
});
|
||||
shutdown_tx
|
||||
}
|
||||
|
||||
pub fn get_flow_tracker(&self) -> Arc<FlowTracker> {
|
||||
self.flow_tracker.clone()
|
||||
}
|
||||
|
||||
async fn run_inference_loop(&self, mut shutdown_rx: oneshot::Receiver<()>) {
|
||||
let mut ticker = interval(Duration::from_secs(self.inference_interval_secs));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut shutdown_rx => break,
|
||||
_ = ticker.tick() => {}
|
||||
}
|
||||
|
||||
let total_flows = self.flow_tracker.flow_count();
|
||||
let all_flows = self.flow_tracker.get_flows_snapshot();
|
||||
let packet_counts: Vec<usize> = all_flows.iter().map(|f| f.packet_count()).collect();
|
||||
|
||||
let flows = self.flow_tracker.get_flows_for_inference(self.min_packets);
|
||||
|
||||
log!(MLLog::FlowStats(
|
||||
total_flows,
|
||||
flows.len(),
|
||||
self.min_packets,
|
||||
format!("{:?}", packet_counts)
|
||||
));
|
||||
|
||||
if flows.is_empty() {
|
||||
log!(MLLog::InferenceSkipped(format!(
|
||||
"No flows with sufficient packets (total flows: {}, min packets: {})",
|
||||
total_flows, self.min_packets
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ref logger) = self.traffic_logger {
|
||||
let feature_names = FlowFeatures::all_feature_names_owned();
|
||||
for flow in &flows {
|
||||
let features = FlowFeatures::extract(flow, &feature_names);
|
||||
logger.log_row(features.to_csv_record());
|
||||
}
|
||||
self.flow_tracker.cleanup_old_flows(60_000_000);
|
||||
continue;
|
||||
}
|
||||
|
||||
let batch = &flows[..flows.len().min(self.batch_size)];
|
||||
|
||||
log!(MLLog::RunningInference(batch.len()));
|
||||
|
||||
let start = Instant::now();
|
||||
let results = self.inference_pipeline.infer_batch(batch);
|
||||
let elapsed_us = start.elapsed().as_micros() as u64;
|
||||
|
||||
let stats = InferenceStats::from_results(&results, elapsed_us);
|
||||
|
||||
if results.len() != batch.len() {
|
||||
log!(MLLog::InferenceResults(batch.len(), results.len()));
|
||||
}
|
||||
|
||||
log!(MLLog::InferenceCompleted(
|
||||
stats.total_flows,
|
||||
stats.malicious_flows,
|
||||
stats.benign_flows,
|
||||
(elapsed_us as f64 / 1000.0) as u32,
|
||||
stats.flows_per_second
|
||||
));
|
||||
|
||||
if let Ok(mut aggregator) = self.aggregator.lock() {
|
||||
for result in &results {
|
||||
if result.is_attack {
|
||||
let should_alert =
|
||||
aggregator.should_alert(&result.flow_key_raw, result.ae_score, result.threshold);
|
||||
|
||||
if should_alert {
|
||||
log!(MLLog::ThreatDetected(
|
||||
format!("{:?}", result.direction),
|
||||
result.flow_key.clone(),
|
||||
result.attack_type.clone().unwrap_or_else(|| "UNKNOWN".to_string()),
|
||||
result.confidence,
|
||||
result.ae_score,
|
||||
));
|
||||
|
||||
self.ml_alert.broadcast_alert(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aggregator.cleanup();
|
||||
}
|
||||
|
||||
self.flow_tracker.cleanup_old_flows(60_000_000);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_packet(&self, 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(&[]);
|
||||
self.flow_tracker.process_packet(packet_info, is_ingress, payload);
|
||||
}
|
||||
None => log!(MLLog::ParsePacketFailed(packet_data.len())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_stats(&self) -> EngineStats {
|
||||
EngineStats {
|
||||
active_flows: self.flow_tracker.flow_count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PacketProcessor {
|
||||
ml_engine: Arc<Engine>,
|
||||
}
|
||||
|
||||
impl PacketProcessor {
|
||||
pub fn new(ml_engine: Arc<Engine>) -> Self {
|
||||
Self { ml_engine }
|
||||
}
|
||||
|
||||
pub fn process(&self, packet_data: &[u8], is_ingress: bool) {
|
||||
self.ml_engine.process_packet(packet_data, is_ingress);
|
||||
}
|
||||
|
||||
pub fn process_batch(&self, packets: &[Vec<u8>], is_ingress: bool) {
|
||||
for packet in packets {
|
||||
self.process(packet, is_ingress);
|
||||
}
|
||||
}
|
||||
}
|
||||
359
net-guardia/src/ml/feature_extractor.rs
Normal file
359
net-guardia/src/ml/feature_extractor.rs
Normal file
@ -0,0 +1,359 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::flow_tracker::FlowData;
|
||||
use crate::model::ml_detection::{ClipParams, PacketData};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlowFeatures {
|
||||
pub features: Vec<f64>,
|
||||
pub feature_num: usize,
|
||||
}
|
||||
|
||||
impl FlowFeatures {
|
||||
pub fn extract(flow: &FlowData, feature_names: &[String]) -> Self {
|
||||
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());
|
||||
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.psh).count() as f64;
|
||||
let bwd_psh = flow.bwd_packets.iter().filter(|p| p.flags.psh).count() as f64;
|
||||
let fwd_urg = flow.fwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
|
||||
let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).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.header_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 {
|
||||
self.features[i] = (self.features[i] - means[i]) / stds[i];
|
||||
} else {
|
||||
self.features[i] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clip(&mut self, clip_min: f64, clip_max: f64) {
|
||||
for i in 0..self.feature_num {
|
||||
self.features[i] = self.features[i].max(clip_min).min(clip_max);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn winsorize(&mut self, clip_params: &HashMap<String, ClipParams>, feature_names: &[String]) {
|
||||
for (i, feature_name) in feature_names.iter().enumerate() {
|
||||
if i < self.feature_num {
|
||||
if let Some(params) = clip_params.get(feature_name) {
|
||||
self.features[i] = self.features[i].clamp(params.lower, params.upper);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_features_content(&self) -> &Vec<f64> {
|
||||
&self.features
|
||||
}
|
||||
|
||||
pub fn all_feature_names() -> Vec<&'static str> {
|
||||
vec![
|
||||
"Destination Port",
|
||||
"Protocol",
|
||||
"Flow Duration",
|
||||
"Total Fwd Packets",
|
||||
"Total Backward Packets",
|
||||
"Total Length of Fwd Packets",
|
||||
"Total Length of Bwd Packets",
|
||||
"Fwd Packet Length Max",
|
||||
"Fwd Packet Length Min",
|
||||
"Fwd Packet Length Mean",
|
||||
"Fwd Packet Length Std",
|
||||
"Bwd Packet Length Max",
|
||||
"Bwd Packet Length Min",
|
||||
"Bwd Packet Length Mean",
|
||||
"Bwd Packet Length Std",
|
||||
"Flow Bytes/s",
|
||||
"Flow Packets/s",
|
||||
"Flow IAT Mean",
|
||||
"Flow IAT Std",
|
||||
"Flow IAT Max",
|
||||
"Flow IAT Min",
|
||||
"Fwd IAT Total",
|
||||
"Fwd IAT Mean",
|
||||
"Fwd IAT Std",
|
||||
"Fwd IAT Max",
|
||||
"Fwd IAT Min",
|
||||
"Bwd IAT Total",
|
||||
"Bwd IAT Mean",
|
||||
"Bwd IAT Std",
|
||||
"Bwd IAT Max",
|
||||
"Bwd IAT Min",
|
||||
"Fwd PSH Flags",
|
||||
"Bwd PSH Flags",
|
||||
"Fwd URG Flags",
|
||||
"Bwd URG Flags",
|
||||
"Fwd Header Length",
|
||||
"Bwd Header Length",
|
||||
"Fwd Packets/s",
|
||||
"Bwd Packets/s",
|
||||
"Min Packet Length",
|
||||
"Max Packet Length",
|
||||
"Packet Length Mean",
|
||||
"Packet Length Std",
|
||||
"Packet Length Variance",
|
||||
"FIN Flag Count",
|
||||
"SYN Flag Count",
|
||||
"RST Flag Count",
|
||||
"PSH Flag Count",
|
||||
"ACK Flag Count",
|
||||
"URG Flag Count",
|
||||
"CWE Flag Count",
|
||||
"ECE Flag Count",
|
||||
"Down/Up Ratio",
|
||||
"Average Packet Size",
|
||||
"Avg Fwd Segment Size",
|
||||
"Avg Bwd Segment Size",
|
||||
"Fwd Header Length.1",
|
||||
"Fwd Avg Bytes/Bulk",
|
||||
"Fwd Avg Packets/Bulk",
|
||||
"Fwd Avg Bulk Rate",
|
||||
"Bwd Avg Bytes/Bulk",
|
||||
"Bwd Avg Packets/Bulk",
|
||||
"Bwd Avg Bulk Rate",
|
||||
"Subflow Fwd Packets",
|
||||
"Subflow Fwd Bytes",
|
||||
"Subflow Bwd Packets",
|
||||
"Subflow Bwd Bytes",
|
||||
"Init_Win_bytes_forward",
|
||||
"Init_Win_bytes_backward",
|
||||
"act_data_pkt_fwd",
|
||||
"min_seg_size_forward",
|
||||
"Active Mean",
|
||||
"Active Std",
|
||||
"Active Max",
|
||||
"Active Min",
|
||||
"Idle Mean",
|
||||
"Idle Std",
|
||||
"Idle Max",
|
||||
"Idle Min",
|
||||
]
|
||||
}
|
||||
|
||||
pub fn all_feature_names_owned() -> Vec<String> {
|
||||
Self::all_feature_names().iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
pub fn to_csv_record(&self) -> Vec<String> {
|
||||
let mut record: Vec<String> = self.features.iter().map(|f| f.to_string()).collect();
|
||||
record.push("BENIGN".to_string());
|
||||
record
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_stats(values: &[f64]) -> (f64, f64, f64, f64) {
|
||||
if values.is_empty() {
|
||||
return (0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
let n = values.len() as f64;
|
||||
let sum: f64 = values.iter().sum();
|
||||
let mean = sum / n;
|
||||
|
||||
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 std = variance.sqrt();
|
||||
|
||||
(max, min, mean, std)
|
||||
}
|
||||
|
||||
fn compute_iats(packets: &[PacketData]) -> Vec<f64> {
|
||||
if packets.len() < 2 {
|
||||
return vec![0.0];
|
||||
}
|
||||
|
||||
packets
|
||||
.windows(2)
|
||||
.map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn compute_flow_iats(fwd_packets: &[PacketData], bwd_packets: &[PacketData]) -> Vec<f64> {
|
||||
let mut all_packets: Vec<&PacketData> = fwd_packets.iter().chain(bwd_packets.iter()).collect();
|
||||
all_packets.sort_by_key(|p| p.timestamp_us);
|
||||
|
||||
if all_packets.len() < 2 {
|
||||
return vec![0.0];
|
||||
}
|
||||
|
||||
all_packets
|
||||
.windows(2)
|
||||
.map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64)
|
||||
.collect()
|
||||
}
|
||||
352
net-guardia/src/ml/flow_tracker.rs
Normal file
352
net-guardia/src/ml/flow_tracker.rs
Normal file
@ -0,0 +1,352 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time;
|
||||
|
||||
use common::model::event::Event;
|
||||
|
||||
use crate::model::direction::Direction;
|
||||
use crate::model::ml_detection::{BulkState, FlowKey, PacketData};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlowData {
|
||||
pub flow_key: FlowKey,
|
||||
pub direction: Direction,
|
||||
pub start_time_us: u64,
|
||||
pub last_time_us: u64,
|
||||
pub fwd_packets: Vec<PacketData>,
|
||||
pub fwd_total_bytes: u64,
|
||||
pub fwd_header_bytes: u64,
|
||||
pub bwd_packets: Vec<PacketData>,
|
||||
pub bwd_total_bytes: u64,
|
||||
pub bwd_header_bytes: u64,
|
||||
pub fin_count: u32,
|
||||
pub syn_count: u32,
|
||||
pub rst_count: u32,
|
||||
pub psh_count: u32,
|
||||
pub ack_count: u32,
|
||||
pub urg_count: u32,
|
||||
pub cwe_count: u32,
|
||||
pub ece_count: u32,
|
||||
pub init_win_bytes_fwd: u16,
|
||||
pub init_win_bytes_bwd: u16,
|
||||
pub active_periods: Vec<u64>,
|
||||
pub idle_periods: Vec<u64>,
|
||||
pub last_packet_time: u64,
|
||||
pub fwd_bulk_state: BulkState,
|
||||
pub bwd_bulk_state: BulkState,
|
||||
}
|
||||
|
||||
impl FlowData {
|
||||
pub fn new(flow_key: FlowKey, first_packet: &Event, direction: Direction) -> Self {
|
||||
Self {
|
||||
flow_key,
|
||||
direction,
|
||||
start_time_us: first_packet.timestamp_us(),
|
||||
last_time_us: first_packet.timestamp_us(),
|
||||
fwd_packets: Vec::new(),
|
||||
fwd_total_bytes: 0,
|
||||
fwd_header_bytes: 0,
|
||||
bwd_packets: Vec::new(),
|
||||
bwd_total_bytes: 0,
|
||||
bwd_header_bytes: 0,
|
||||
fin_count: 0,
|
||||
syn_count: 0,
|
||||
rst_count: 0,
|
||||
psh_count: 0,
|
||||
ack_count: 0,
|
||||
urg_count: 0,
|
||||
cwe_count: 0,
|
||||
ece_count: 0,
|
||||
init_win_bytes_fwd: if first_packet.is_forward() {
|
||||
first_packet.tcp_window_size()
|
||||
} else {
|
||||
0
|
||||
},
|
||||
init_win_bytes_bwd: if !first_packet.is_forward() {
|
||||
first_packet.tcp_window_size()
|
||||
} else {
|
||||
0
|
||||
},
|
||||
active_periods: Vec::new(),
|
||||
idle_periods: Vec::new(),
|
||||
last_packet_time: first_packet.timestamp_us(),
|
||||
fwd_bulk_state: BulkState::default(),
|
||||
bwd_bulk_state: BulkState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_packet(&mut self, packet: &Event) {
|
||||
let packet_data = PacketData {
|
||||
timestamp_us: packet.timestamp_us(),
|
||||
length: packet.packet_length(),
|
||||
header_length: packet.header_length(),
|
||||
payload_length: packet.payload_length(),
|
||||
flags: packet.tcp_flags().clone(),
|
||||
};
|
||||
|
||||
if packet.tcp_flags().fin {
|
||||
self.fin_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().syn {
|
||||
self.syn_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().rst {
|
||||
self.rst_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().psh {
|
||||
self.psh_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().ack {
|
||||
self.ack_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().urg {
|
||||
self.urg_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().cwr {
|
||||
self.cwe_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().ece {
|
||||
self.ece_count += 1;
|
||||
}
|
||||
|
||||
let iat = packet.timestamp_us().saturating_sub(self.last_packet_time);
|
||||
const IDLE_THRESHOLD_US: u64 = 1_000_000;
|
||||
|
||||
if iat > IDLE_THRESHOLD_US {
|
||||
self.idle_periods.push(iat);
|
||||
} else if iat > 0 {
|
||||
self.active_periods.push(iat);
|
||||
}
|
||||
|
||||
self.last_packet_time = packet.timestamp_us();
|
||||
self.last_time_us = packet.timestamp_us();
|
||||
|
||||
if packet.is_forward() {
|
||||
self.fwd_packets.push(packet_data.clone());
|
||||
self.fwd_total_bytes += packet.packet_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);
|
||||
} else {
|
||||
self.bwd_packets.push(packet_data.clone());
|
||||
self.bwd_total_bytes += packet.packet_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);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_bulk_state(bulk_state: &mut BulkState, packet: &PacketData) {
|
||||
const BULK_MIN_PACKETS: u64 = 4;
|
||||
const BULK_MIN_BYTES: u64 = 1000;
|
||||
|
||||
if packet.payload_length > 0 {
|
||||
if !bulk_state.in_bulk {
|
||||
bulk_state.in_bulk = true;
|
||||
bulk_state.last_bulk_bytes = packet.length as u64;
|
||||
bulk_state.last_bulk_packets = 1;
|
||||
bulk_state.last_bulk_start_us = packet.timestamp_us;
|
||||
bulk_state.last_bulk_packet_us = packet.timestamp_us;
|
||||
} else {
|
||||
bulk_state.last_bulk_bytes += packet.length as u64;
|
||||
bulk_state.last_bulk_packets += 1;
|
||||
bulk_state.last_bulk_packet_us = packet.timestamp_us;
|
||||
}
|
||||
} else {
|
||||
if bulk_state.in_bulk
|
||||
&& bulk_state.last_bulk_packets >= BULK_MIN_PACKETS
|
||||
&& bulk_state.last_bulk_bytes >= BULK_MIN_BYTES
|
||||
{
|
||||
bulk_state.bulk_count += 1;
|
||||
bulk_state.total_bytes += bulk_state.last_bulk_bytes;
|
||||
bulk_state.total_packets += bulk_state.last_bulk_packets;
|
||||
bulk_state.total_duration_us += bulk_state
|
||||
.last_bulk_packet_us
|
||||
.saturating_sub(bulk_state.last_bulk_start_us);
|
||||
}
|
||||
bulk_state.in_bulk = false;
|
||||
bulk_state.last_bulk_bytes = 0;
|
||||
bulk_state.last_bulk_packets = 0;
|
||||
bulk_state.last_bulk_start_us = 0;
|
||||
bulk_state.last_bulk_packet_us = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duration_us(&self) -> u64 {
|
||||
self.last_time_us.saturating_sub(self.start_time_us)
|
||||
}
|
||||
|
||||
pub fn packet_count(&self) -> usize {
|
||||
self.fwd_packets.len() + self.bwd_packets.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FlowTracker {
|
||||
flows: Arc<Mutex<HashMap<FlowKey, FlowData>>>,
|
||||
max_flows: usize,
|
||||
}
|
||||
|
||||
impl FlowTracker {
|
||||
pub fn new(max_flows: usize) -> Self {
|
||||
Self {
|
||||
flows: Arc::new(Mutex::new(HashMap::new())),
|
||||
max_flows,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_packet(&self, mut packet: Event, is_ingress: bool, payload: &[u8]) {
|
||||
let direction = if is_ingress {
|
||||
Direction::Ingress
|
||||
} else {
|
||||
Direction::Egress
|
||||
};
|
||||
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 Ok(mut flows) = self.flows.lock() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Try-both: canonical key is whichever orientation already exists in the flow table.
|
||||
// For new flows, identify the initiator using (in priority order):
|
||||
// 1. TCP SYN / SYN+ACK flags
|
||||
// 2. DPI: TLS ClientHello/ServerHello, HTTP request/response, DNS QR bit
|
||||
// 3. Best effort: use packet as-is
|
||||
let (actual_key, is_forward) = if flows.contains_key(&packet_key) {
|
||||
(packet_key, true)
|
||||
} else if flows.contains_key(&reversed_key) {
|
||||
(reversed_key, false)
|
||||
} else {
|
||||
let flags = packet.tcp_flags();
|
||||
if flags.syn && flags.ack {
|
||||
// Normal: Server (egress side) sends SYN+ACK, packet arrives on ingress → reverse
|
||||
// Bot attack: Client (egress side) sends SYN+ACK, packet arrives on egress → keep as-is
|
||||
if is_ingress {
|
||||
(reversed_key, false)
|
||||
} else {
|
||||
(packet_key, true)
|
||||
}
|
||||
} else if flags.syn {
|
||||
(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),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
packet.set_is_forward(is_forward);
|
||||
|
||||
// `direction` should reflect the initiator's interface.
|
||||
// If this packet is backward (is_forward = false), the initiator is on the opposite side.
|
||||
let initiator_direction = if is_forward { direction } else { direction.flip() };
|
||||
|
||||
let flow = flows
|
||||
.entry(actual_key.clone())
|
||||
.or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction));
|
||||
|
||||
flow.add_packet(&packet);
|
||||
|
||||
if flows.len() > self.max_flows {
|
||||
if let Some(key) = flows.keys().next().cloned() {
|
||||
flows.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_flows_snapshot(&self) -> Vec<FlowData> {
|
||||
let Ok(flows) = self.flows.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
flows.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn get_flows_for_inference(&self, min_packets: usize) -> Vec<FlowData> {
|
||||
let Ok(flows) = self.flows.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
flows
|
||||
.values()
|
||||
.filter(|flow| flow.packet_count() >= min_packets)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn cleanup_old_flows(&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);
|
||||
|
||||
let Ok(mut flows) = self.flows.lock() else {
|
||||
return;
|
||||
};
|
||||
flows.retain(|_, flow| now.saturating_sub(flow.last_time_us) < max_age_us);
|
||||
}
|
||||
|
||||
pub fn flow_count(&self) -> usize {
|
||||
let Ok(flows) = self.flows.lock() else {
|
||||
return 0;
|
||||
};
|
||||
flows.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect payload bytes to determine which side is the flow initiator.
|
||||
/// Returns Some(true) if this packet is from the initiator, Some(false) if from the responder,
|
||||
/// or None if the payload gives no useful signal.
|
||||
fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16) -> Option<bool> {
|
||||
if payload.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TLS: record type 0x16 (Handshake), byte 5 = handshake type
|
||||
// 0x01 = ClientHello → this side is the initiator
|
||||
// 0x02 = ServerHello → this side is the responder
|
||||
if payload.len() >= 6 && payload[0] == 0x16 {
|
||||
return match payload[5] {
|
||||
0x01 => Some(true),
|
||||
0x02 => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
// HTTP: request line starts with a method verb (initiator),
|
||||
// response starts with "HTTP/" (responder)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// DNS over UDP (port 53): flags byte 2, MSB = QR bit
|
||||
// 0 = query (initiator), 1 = response (responder)
|
||||
if protocol == 17 && (src_port == 53 || dst_port == 53) && payload.len() >= 3 {
|
||||
return Some((payload[2] >> 7) == 0);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
140
net-guardia/src/ml/inference.rs
Normal file
140
net-guardia/src/ml/inference.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use macros::log;
|
||||
use tract_onnx::prelude::*;
|
||||
|
||||
use super::config_loader::InferenceConfig;
|
||||
use super::feature_extractor::FlowFeatures;
|
||||
use super::flow_tracker::FlowData;
|
||||
use super::model_loader::MLModels;
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::ml_detection::DetectionResult;
|
||||
|
||||
pub struct Inference {
|
||||
pub models: Arc<MLModels>,
|
||||
pub config: Arc<InferenceConfig>,
|
||||
}
|
||||
|
||||
impl Inference {
|
||||
pub fn new(models: Arc<MLModels>, config: Arc<InferenceConfig>) -> Self {
|
||||
Self { models, config }
|
||||
}
|
||||
|
||||
pub fn infer_batch(&self, flows: &[FlowData]) -> Vec<DetectionResult> {
|
||||
flows.iter().filter_map(|flow| self.infer_single(flow)).collect()
|
||||
}
|
||||
|
||||
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,
|
||||
Err(e) => {
|
||||
log!(MLLog::InferenceFailed("DeepAutoEncoder".to_string(), e.to_string()));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let cls_input = self.build_classifier_input(&ae_features, ae_score);
|
||||
|
||||
let (attack_type, confidence) = match self.run_classifier(&cls_input) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
log!(MLLog::InferenceFailed("LightGBM".to_string(), e.to_string()));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let is_attack = ae_score >= self.config.ae_threshold;
|
||||
|
||||
let flow_key = format!(
|
||||
"{}:{} -> {}:{} (proto {}) [{}]",
|
||||
flow.flow_key.src_ip,
|
||||
flow.flow_key.src_port,
|
||||
flow.flow_key.dst_ip,
|
||||
flow.flow_key.dst_port,
|
||||
flow.flow_key.protocol,
|
||||
flow.direction
|
||||
);
|
||||
|
||||
Some(DetectionResult {
|
||||
flow_key,
|
||||
flow_key_raw: flow.flow_key.clone(),
|
||||
direction: flow.direction,
|
||||
is_attack,
|
||||
attack_type: if is_attack { Some(attack_type) } else { None },
|
||||
confidence,
|
||||
ae_score,
|
||||
threshold: self.config.ae_threshold,
|
||||
})
|
||||
}
|
||||
|
||||
fn preprocess_ae_features(&self, flow: &FlowData) -> Vec<f32> {
|
||||
let mut features = FlowFeatures::extract(flow, &self.config.ae_feature_names);
|
||||
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);
|
||||
features.features.iter().map(|&x| x as f32).collect()
|
||||
}
|
||||
|
||||
fn vec_to_array2(v: &[f32]) -> tract_ndarray::Array2<f32> {
|
||||
tract_ndarray::Array2::from_shape_fn((1, v.len()), |(_, j)| v[j])
|
||||
}
|
||||
|
||||
/// Classifier 輸入 = 已預處理的 ae_features ++ [ae_anomaly_score]
|
||||
fn build_classifier_input(&self, ae_features: &[f32], ae_score: f32) -> tract_ndarray::Array2<f32> {
|
||||
let n = ae_features.len() + 1;
|
||||
tract_ndarray::Array2::from_shape_fn((1, n), |(_, j)| {
|
||||
if j < ae_features.len() {
|
||||
ae_features[j]
|
||||
} else {
|
||||
ae_score
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn run_autoencoder(&self, input: &tract_ndarray::Array2<f32>) -> TractResult<f32> {
|
||||
let result = self
|
||||
.models
|
||||
.deep_autoencoder
|
||||
.run(tvec![input.clone().into_tensor().into()])?;
|
||||
|
||||
let output = result[0]
|
||||
.to_array_view::<f32>()?
|
||||
.into_dimensionality::<tract_ndarray::Ix2>()?;
|
||||
|
||||
let diff = input - &output;
|
||||
let mse = (&diff * &diff).sum() / self.config.ae_feature_names.len() as f32;
|
||||
|
||||
Ok(mse)
|
||||
}
|
||||
|
||||
fn run_classifier(&self, input: &tract_ndarray::Array2<f32>) -> TractResult<(String, f32)> {
|
||||
let input_tensor = input.clone().into_tensor();
|
||||
let result = self.models.classifier.run(tvec![input_tensor.into()])?;
|
||||
|
||||
let output = result[0].to_array_view::<f32>()?;
|
||||
|
||||
let mut max_prob: f32 = 0.0;
|
||||
let mut predicted_class: usize = 0;
|
||||
|
||||
for (i, &prob) in output.iter().enumerate() {
|
||||
if prob > max_prob {
|
||||
max_prob = prob;
|
||||
predicted_class = i;
|
||||
}
|
||||
}
|
||||
|
||||
let attack_type = self
|
||||
.config
|
||||
.attack_labels
|
||||
.get(&predicted_class.to_string())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "UNKNOWN".to_string());
|
||||
|
||||
Ok((attack_type, max_prob))
|
||||
}
|
||||
}
|
||||
8
net-guardia/src/ml/mod.rs
Normal file
8
net-guardia/src/ml/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod model_loader;
|
||||
pub mod config_loader;
|
||||
pub mod flow_tracker;
|
||||
pub mod feature_extractor;
|
||||
pub mod inference;
|
||||
pub mod engine;
|
||||
pub mod aggregator;
|
||||
pub mod traffic_logger;
|
||||
60
net-guardia/src/ml/model_loader.rs
Normal file
60
net-guardia/src/ml/model_loader.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use tract_onnx::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::model::error::ml::MLError;
|
||||
use crate::model::ml_detection::RunnableModel;
|
||||
|
||||
use super::config_loader::InferenceConfig;
|
||||
|
||||
pub struct MLModels {
|
||||
pub deep_autoencoder: RunnableModel,
|
||||
pub classifier: RunnableModel,
|
||||
}
|
||||
impl MLModels {
|
||||
pub fn load_models(app_config: &Arc<AppConfig>, inference_config: &Arc<InferenceConfig>) -> Result<Self, MLError> {
|
||||
Ok(Self {
|
||||
deep_autoencoder: Self::loader(&app_config.deep_autoencoder_name, inference_config.num_ae_features())?,
|
||||
classifier: Self::loader(&app_config.classifier_name, inference_config.num_classifier_features())?
|
||||
})
|
||||
}
|
||||
|
||||
pub fn loader(model: &str, features: usize) -> Result<RunnableModel, MLError> {
|
||||
let model_path = PathBuf::from("models").join(model);
|
||||
|
||||
let mut model = onnx()
|
||||
.model_for_path(&model_path)
|
||||
.map_err(|_| {
|
||||
MLError::ModelLoadFailed { path: model_path.clone() }
|
||||
})?;
|
||||
|
||||
model.set_input_fact(0, f32::fact(&[1, features]).into())
|
||||
.map_err(|_| {
|
||||
MLError::ModelLoadFailed { path: model_path.clone() }
|
||||
})?;
|
||||
|
||||
let runnable_model = model
|
||||
.into_optimized()
|
||||
.map_err(|_| {
|
||||
MLError::ModelLoadFailed { path: model_path.clone() }
|
||||
})?
|
||||
.into_runnable()
|
||||
.map_err(|_| {
|
||||
MLError::ModelLoadFailed { path: model_path }
|
||||
})?;
|
||||
|
||||
Ok(runnable_model)
|
||||
}
|
||||
|
||||
pub fn get_model_info(&self, name: &str) -> String {
|
||||
let model = match name {
|
||||
"deep_autoencoder" => &self.deep_autoencoder,
|
||||
"classifier" => &self.classifier,
|
||||
_ => return "unknown model".to_string(),
|
||||
};
|
||||
|
||||
let inputs = model.model().inputs.len();
|
||||
let outputs = model.model().outputs.len();
|
||||
format!("{}: inputs: {}, outputs: {}", name, inputs, outputs)
|
||||
}
|
||||
}
|
||||
46
net-guardia/src/ml/traffic_logger.rs
Normal file
46
net-guardia/src/ml/traffic_logger.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::thread;
|
||||
|
||||
use crossbeam::channel::{bounded, Sender, TrySendError};
|
||||
|
||||
pub struct TrafficLogger {
|
||||
sender: Sender<Vec<String>>,
|
||||
}
|
||||
|
||||
impl TrafficLogger {
|
||||
pub fn new(csv_path: &str, header: Vec<String>) -> Result<Self, std::io::Error> {
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(csv_path)?;
|
||||
|
||||
let mut writer = BufWriter::new(file);
|
||||
writeln!(writer, "{}", header.join(","))?;
|
||||
writer.flush()?;
|
||||
|
||||
let (sender, receiver) = bounded::<Vec<String>>(65536);
|
||||
|
||||
thread::Builder::new()
|
||||
.name("traffic-logger".to_string())
|
||||
.spawn(move || {
|
||||
for record in receiver {
|
||||
if let Err(e) = writeln!(writer, "{}", record.join(",")) {
|
||||
eprintln!("[traffic-logger] write error: {}", e);
|
||||
}
|
||||
}
|
||||
let _ = writer.flush();
|
||||
})?;
|
||||
|
||||
Ok(Self { sender })
|
||||
}
|
||||
|
||||
pub fn log_row(&self, record: Vec<String>) {
|
||||
match self.sender.try_send(record) {
|
||||
Ok(_) => {}
|
||||
Err(TrySendError::Full(_)) => {}
|
||||
Err(TrySendError::Disconnected(_)) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
net-guardia/src/model/config.rs
Normal file
34
net-guardia/src/model/config.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigTable {
|
||||
#[serde(rename = "Config")]
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub ingress_ifname: String,
|
||||
pub egress_ifname: String,
|
||||
pub geoip_db_name: String,
|
||||
pub deep_autoencoder_name: String,
|
||||
pub classifier_name: String,
|
||||
pub models_config_name: String,
|
||||
pub combined_queue_count: u32,
|
||||
pub channel_size: usize,
|
||||
pub fill_queue_size: u32,
|
||||
pub comp_queue_size: u32,
|
||||
pub tx_queue_size: u32,
|
||||
pub rx_queue_size: u32,
|
||||
pub frame_size: u32,
|
||||
pub frame_count: u32,
|
||||
pub refresh_interval: u64,
|
||||
pub http_server_bind_port: u16,
|
||||
pub max_concurrent_flows: usize,
|
||||
pub min_packets_for_inference: usize,
|
||||
pub inference_interval_secs: u64,
|
||||
pub aggregator_window_secs: u64,
|
||||
pub inference_batch_size: usize,
|
||||
pub traffic_logging_mode: bool,
|
||||
pub traffic_log_csv_path: String,
|
||||
}
|
||||
33
net-guardia/src/model/direction.rs
Normal file
33
net-guardia/src/model/direction.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Direction {
|
||||
Ingress,
|
||||
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 {
|
||||
Direction::Ingress => write!(f, "Ingress"),
|
||||
Direction::Egress => write!(f, "Egress"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FlowDirection {
|
||||
Source,
|
||||
Destination,
|
||||
}
|
||||
69
net-guardia/src/model/error/ebpf.rs
Normal file
69
net-guardia/src/model/error/ebpf.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use macros::traceable;
|
||||
use tracing;
|
||||
|
||||
traceable! {
|
||||
EbpfError {
|
||||
#[error("Failed to initialize eBPF logger")]
|
||||
LoggerInitFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Ebpf program not found")]
|
||||
EbpfNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to load XDP program")]
|
||||
ProgramNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to load XDP program")]
|
||||
GetProgramFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to load XDP program")]
|
||||
LoadProgramFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to attach the XDP program")]
|
||||
AttachProgramFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to set umem")]
|
||||
UmemSetFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to set AF_XDP socket")]
|
||||
SocketSetFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to set AF_XDP")]
|
||||
AfXdpSetFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to wakeup TX")]
|
||||
WakeupTXFailed => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Map not found")]
|
||||
MapNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[error("An error occurred during map operation")]
|
||||
MapOperationError => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("The ip required for operation does not exist")]
|
||||
IpDoesNotExist => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Amount of rules has reached the upper limit")]
|
||||
RuleReachLimit => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Unknown error")]
|
||||
UnknownError => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to spawn XSK thread")]
|
||||
ThreadSpawnFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Completion queue processing failed")]
|
||||
CompQueueError => tracing::Level::ERROR,
|
||||
|
||||
#[error("RX queue processing failed")]
|
||||
RXQueueError => tracing::Level::ERROR,
|
||||
|
||||
#[error("TX queue processing failed")]
|
||||
TXQueueError => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
14
net-guardia/src/model/error/http.rs
Normal file
14
net-guardia/src/model/error/http.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
HttpError {
|
||||
#[error("Bind port error")]
|
||||
BindPortError => tracing::Level::ERROR,
|
||||
|
||||
#[error("Http Server panic")]
|
||||
ServerPanic => tracing::Level::ERROR,
|
||||
|
||||
#[error("WebSocket error")]
|
||||
WebSocketError => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
10
net-guardia/src/model/error/io.rs
Normal file
10
net-guardia/src/model/error/io.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
IOError {
|
||||
#[error("Failed to create directory: {path}")]
|
||||
CreateDirectoryFailed { path: PathBuf } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
29
net-guardia/src/model/error/misc.rs
Normal file
29
net-guardia/src/model/error/misc.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
MiscError {
|
||||
#[no_source]
|
||||
#[error("Failed to remove limit on locked memory, ret is: {ret}")]
|
||||
RamLimitUnlockError { ret: i32 } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to send message to receiver")]
|
||||
SendMessageError => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to serialize data")]
|
||||
SerializeError => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to deserialize data")]
|
||||
DeserializeError => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[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 create traffic log file '{path}': {reason}")]
|
||||
TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
23
net-guardia/src/model/error/ml.rs
Normal file
23
net-guardia/src/model/error/ml.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
MLError {
|
||||
#[no_source]
|
||||
#[error("Initialize Machine Learning detection failed")]
|
||||
InitializeFailed => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to load ONNX model from: {path:?}")]
|
||||
ModelLoadFailed { path: PathBuf } => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to load inference configuration from: {path:?}")]
|
||||
ConfigLoadFailed { path: PathBuf } => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to parse inference configuration: {reason}")]
|
||||
ConfigParseFailed { reason: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
67
net-guardia/src/model/error/mod.rs
Normal file
67
net-guardia/src/model/error/mod.rs
Normal file
@ -0,0 +1,67 @@
|
||||
pub mod ebpf;
|
||||
pub mod http;
|
||||
pub mod io;
|
||||
pub mod misc;
|
||||
pub mod ml;
|
||||
pub mod system;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::io::IOError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::ml::MLError;
|
||||
use crate::model::error::system::SystemError;
|
||||
|
||||
#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Ebpf(EbpfError),
|
||||
#[error("{0}")]
|
||||
Http(HttpError),
|
||||
#[error("{0}")]
|
||||
ML(MLError),
|
||||
#[error("{0}")]
|
||||
IO(IOError),
|
||||
#[error("{0}")]
|
||||
Misc(MiscError),
|
||||
#[error("{0}")]
|
||||
System(SystemError),
|
||||
}
|
||||
|
||||
impl From<EbpfError> for Error {
|
||||
fn from(error: EbpfError) -> Self {
|
||||
Self::Ebpf(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for Error {
|
||||
fn from(error: HttpError) -> Self {
|
||||
Self::Http(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IOError> for Error {
|
||||
fn from(error: IOError) -> Self {
|
||||
Self::IO(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MiscError> for Error {
|
||||
fn from(error: MiscError) -> Self {
|
||||
Self::Misc(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemError> for Error {
|
||||
fn from(error: SystemError) -> Self {
|
||||
Self::System(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MLError> for Error {
|
||||
fn from(error: MLError) -> Self {
|
||||
Self::ML(error)
|
||||
}
|
||||
}
|
||||
33
net-guardia/src/model/error/system.rs
Normal file
33
net-guardia/src/model/error/system.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
SystemError {
|
||||
#[no_source]
|
||||
#[error("Unable to run as administrator")]
|
||||
RunAsAdminFailed => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Invalid configuration")]
|
||||
InvalidConfig => tracing::Level::ERROR,
|
||||
|
||||
#[error("Configuration not found")]
|
||||
ConfigNotFound => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to terminate instance")]
|
||||
TerminateError => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to send shutdown signal")]
|
||||
ShutdownSignalFailed => tracing::Level::ERROR,
|
||||
|
||||
#[error("Unexcepted thread panic")]
|
||||
ThreadPanic => tracing::Level::ERROR,
|
||||
|
||||
#[error("Unexcepted error")]
|
||||
UnexpectError => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Unknown error")]
|
||||
UnknownError => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
19
net-guardia/src/model/geo_stats.rs
Normal file
19
net-guardia/src/model/geo_stats.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use common::model::flow_stats::FlowStats;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FlowStatsWithGeo {
|
||||
#[serde(flatten)]
|
||||
pub stats: FlowStats,
|
||||
pub geo: Option<GeoLocation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeoLocation {
|
||||
pub country: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
81
net-guardia/src/model/health.rs
Normal file
81
net-guardia/src/model/health.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SystemHealthMetrics {
|
||||
pub timestamp: u64,
|
||||
pub boot_time: u64,
|
||||
pub uptime_seconds: u64,
|
||||
pub system_info: SystemInfo,
|
||||
pub cpu_details: CpuDetails,
|
||||
pub memory_usage: MemoryUsage,
|
||||
pub network_stats: ConfiguredNetworkStats,
|
||||
pub load_average: Option<LoadAverage>,
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SystemInfo {
|
||||
pub kernel_version: Option<String>,
|
||||
pub os_name: Option<String>,
|
||||
pub os_version: Option<String>,
|
||||
pub architecture: String,
|
||||
pub total_processes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CpuDetails {
|
||||
pub cpu_brand: String,
|
||||
pub core_count: usize,
|
||||
pub cpu_usage: f32,
|
||||
pub cpu_frequency: u64,
|
||||
pub cores: Vec<CpuCoreInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CpuCoreInfo {
|
||||
pub core_id: usize,
|
||||
pub usage_percent: f32,
|
||||
pub frequency: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MemoryUsage {
|
||||
pub total: u64,
|
||||
pub used: u64,
|
||||
pub available: u64,
|
||||
pub usage_percent: f32,
|
||||
pub swap_total: u64,
|
||||
pub swap_used: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ConfiguredNetworkStats {
|
||||
pub ingress: Option<NetworkStats>,
|
||||
pub egress: Option<NetworkStats>,
|
||||
// pub management: Option<NetworkStats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct NetworkStats {
|
||||
pub interface: String,
|
||||
pub bytes_received: u64,
|
||||
pub bytes_transmitted: u64,
|
||||
pub packets_received: u64,
|
||||
pub packets_transmitted: u64,
|
||||
pub errors_received: u64,
|
||||
pub errors_transmitted: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct LoadAverage {
|
||||
pub one_minute: f64,
|
||||
pub five_minute: f64,
|
||||
pub fifteen_minute: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SystemHealthStatus {
|
||||
pub overall_healthy: bool,
|
||||
pub issues: Vec<String>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
72
net-guardia/src/model/ip_address.rs
Normal file
72
net-guardia/src/model/ip_address.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use std::hash::Hash;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use common::model::ip_address::*;
|
||||
|
||||
pub trait NativeConvert: Copy {
|
||||
type Native: Eq + PartialEq + Hash;
|
||||
fn into_native(self) -> Self::Native;
|
||||
fn from_native(native: Self::Native) -> Self;
|
||||
}
|
||||
|
||||
impl NativeConvert for IPv4 {
|
||||
type Native = Ipv4Addr;
|
||||
|
||||
fn into_native(self) -> Self::Native {
|
||||
Ipv4Addr::from(u32::from_be(self))
|
||||
}
|
||||
|
||||
fn from_native(native: Self::Native) -> Self {
|
||||
native.to_bits().to_be()
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeConvert for IPv6 {
|
||||
type Native = Ipv6Addr;
|
||||
|
||||
fn into_native(self) -> Self::Native {
|
||||
Ipv6Addr::from(u128::from_be(self))
|
||||
}
|
||||
|
||||
fn from_native(native: Self::Native) -> Self {
|
||||
native.to_bits().to_be()
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeConvert for AddrPortV4 {
|
||||
type Native = SocketAddrV4;
|
||||
|
||||
fn into_native(self) -> Self::Native {
|
||||
SocketAddrV4::new(
|
||||
Ipv4Addr::from(u32::from_be(self.ip())),
|
||||
self.port()
|
||||
)
|
||||
}
|
||||
|
||||
fn from_native(native: Self::Native) -> Self {
|
||||
AddrPortV4::new(
|
||||
native.ip().to_bits().to_be(),
|
||||
native.port()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeConvert for AddrPortV6 {
|
||||
type Native = SocketAddrV6;
|
||||
|
||||
fn into_native(self) -> Self::Native {
|
||||
SocketAddrV6::new(
|
||||
Ipv6Addr::from(u128::from_be(self.ip())),
|
||||
self.port(),
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
fn from_native(native: Self::Native) -> Self {
|
||||
AddrPortV6::new(
|
||||
native.ip().to_bits().to_be(),
|
||||
native.port()
|
||||
)
|
||||
}
|
||||
}
|
||||
10
net-guardia/src/model/list_type.rs
Normal file
10
net-guardia/src/model/list_type.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ListType {
|
||||
#[serde(rename = "white_list")]
|
||||
White,
|
||||
#[serde(rename = "black_list")]
|
||||
Black,
|
||||
}
|
||||
54
net-guardia/src/model/log/ebpf.rs
Normal file
54
net-guardia/src/model/log/ebpf.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use macros::loggable;
|
||||
use tracing;
|
||||
|
||||
loggable! {
|
||||
EbpfLog {
|
||||
#[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("XSK thread shutting down")]
|
||||
XSKShutdown => tracing::Level::INFO,
|
||||
|
||||
#[error("Frame pool exhausted! Pending TX: {send_len} packets")]
|
||||
FramePoolExhausted { send_len: usize } => tracing::Level::WARN,
|
||||
|
||||
#[error("No frames available for TX")]
|
||||
NoFramesAvailable => tracing::Level::WARN,
|
||||
|
||||
#[error("TX wakeup failed: {error}")]
|
||||
TXWakeupFailed { error: String } => tracing::Level::WARN,
|
||||
|
||||
#[error("Completion queue processing error: {error}")]
|
||||
CompQueueError { error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("RX queue processing error: {error}")]
|
||||
RXQueueError { error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("TX queue processing error: {error}")]
|
||||
TXQueueError { error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Failed to spawn thread '{thread_name}': {error}")]
|
||||
ThreadSpawnFailed { thread_name: String, error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Forward channel full, dropping packet")]
|
||||
ForwardChannelFull => tracing::Level::WARN,
|
||||
|
||||
#[error("Forward channel disconnected")]
|
||||
ForwardChannelDisconnected => tracing::Level::ERROR,
|
||||
|
||||
#[error("Fill queue incomplete: produced {produced}, expected {expected}")]
|
||||
FillQueueIncomplete { produced: usize, expected: usize } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
11
net-guardia/src/model/log/health.rs
Normal file
11
net-guardia/src/model/log/health.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use macros::loggable;
|
||||
|
||||
loggable! {
|
||||
Health {
|
||||
#[error("{ifname} interface '{interface}' not found")]
|
||||
InterfaceNotFound { ifname: String, interface: String } => tracing::Level::WARN,
|
||||
|
||||
#[error("Failed to broadcast system health metrics: {error}")]
|
||||
BroadcastFailed { error: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
9
net-guardia/src/model/log/http.rs
Normal file
9
net-guardia/src/model/log/http.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use macros::loggable;
|
||||
use tracing;
|
||||
|
||||
loggable! {
|
||||
HttpLog {
|
||||
#[error("Health WebSocket lagged, skipped {skipped} messages")]
|
||||
WebSocketLaged { skipped: u64 } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
9
net-guardia/src/model/log/misc.rs
Normal file
9
net-guardia/src/model/log/misc.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use macros::loggable;
|
||||
use tracing;
|
||||
|
||||
loggable! {
|
||||
MiscLog {
|
||||
#[error("GeoIP features will be disabled")]
|
||||
GeoIPDisabled => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user