mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
feat: Ed25519 license validation and generator
Validator (net-guardia): Reads license.key (base64 payload + signature), verifies Ed25519 signature against embedded public key, checks expiry. Optional — missing license logs warning, invalid/expired fails startup. GET /api/system/license exposes license info. Generator (license-generator): Standalone crate, not in workspace. Subcommands: keygen (Ed25519 keypair), issue (sign license with device_id/expires/features), verify (check license file). Public key placeholder (all zeros) — replace after running keygen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2e9dbe514f
commit
34d1832e21
7
.gitignore
vendored
7
.gitignore
vendored
@ -26,3 +26,10 @@ net-guardia/static/web
|
||||
# Profiling
|
||||
*.profraw
|
||||
*.profdata
|
||||
|
||||
# License keys
|
||||
license-generator/target/
|
||||
*.hex
|
||||
license.key
|
||||
license_priv.key
|
||||
license_pub.key
|
||||
|
||||
342
Cargo.lock
generated
342
Cargo.lock
generated
@ -318,6 +318,18 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert_matches"
|
||||
version = "1.5.0"
|
||||
@ -463,6 +475,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
@ -504,6 +522,15 @@ version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@ -534,6 +561,12 @@ dependencies = [
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@ -642,6 +675,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
@ -761,12 +800,49 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
@ -818,6 +894,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -855,6 +932,31 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88"
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egress-ebpf"
|
||||
version = "0.1.0"
|
||||
@ -897,6 +999,24 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
@ -1011,8 +1131,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1095,6 +1217,15 @@ dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@ -1315,6 +1446,31 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kstring"
|
||||
version = "2.0.2"
|
||||
@ -1382,6 +1538,17 @@ dependencies = [
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libxdp-sys"
|
||||
version = "0.2.4+1.6.0"
|
||||
@ -1638,14 +1805,18 @@ dependencies = [
|
||||
"actix-cors",
|
||||
"actix-web",
|
||||
"actix-ws",
|
||||
"argon2",
|
||||
"aya",
|
||||
"aya-log",
|
||||
"base64",
|
||||
"cargo_metadata",
|
||||
"common",
|
||||
"crossbeam",
|
||||
"dotenvy",
|
||||
"ed25519-dalek",
|
||||
"futures-util",
|
||||
"ipnetwork",
|
||||
"jsonwebtoken",
|
||||
"libc",
|
||||
"libxdp-sys",
|
||||
"lru",
|
||||
@ -1654,6 +1825,8 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"network-types",
|
||||
"parking_lot",
|
||||
"rand 0.9.2",
|
||||
"rusqlite",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1737,6 +1910,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
@ -1852,12 +2035,33 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@ -1913,6 +2117,16 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@ -2170,6 +2384,34 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.11.0"
|
||||
@ -2413,12 +2655,33 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@ -2451,6 +2714,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@ -2480,6 +2753,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@ -3025,6 +3304,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@ -3055,6 +3340,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@ -3086,6 +3377,51 @@ dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.2"
|
||||
@ -3423,6 +3759,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
|
||||
@ -54,6 +54,9 @@ rusqlite = { version = "0.34", features = ["bundled"] }
|
||||
jsonwebtoken = "9"
|
||||
argon2 = "0.5"
|
||||
rand = "0.9"
|
||||
ed25519-dalek = { version = "2", features = ["std", "rand_core"] }
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# Build dependencies
|
||||
cargo_metadata = { version = "0.23.1", default-features = false }
|
||||
|
||||
@ -30,6 +30,7 @@ traffic_log_csv_path = "traffic_log.csv"
|
||||
[Misc]
|
||||
geoip_db_name = "net-guardia/static/geo/GeoLite2-City.mmdb"
|
||||
database_path = "net-guardia.db"
|
||||
license_file = "license.key"
|
||||
|
||||
[Pipeline]
|
||||
ingress = ["access_control", "rate_limit", "service"]
|
||||
|
||||
13
license-generator/Cargo.toml
Normal file
13
license-generator/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "license-generator"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ed25519-dalek = { version = "2", features = ["std", "rand_core"] }
|
||||
base64 = "0.22"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rand = "0.9"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
pnet = "0.36"
|
||||
217
license-generator/src/main.rs
Normal file
217
license-generator/src/main.rs
Normal file
@ -0,0 +1,217 @@
|
||||
use std::fs;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use clap::{Parser, Subcommand};
|
||||
use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey, Signature};
|
||||
use pnet::datalink;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "license-generator", about = "NetGuardia license generator")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Generate a new Ed25519 keypair
|
||||
Keygen {
|
||||
#[arg(short, long, default_value = "license")]
|
||||
prefix: String,
|
||||
},
|
||||
/// Issue a signed license bound to NIC MACs
|
||||
Issue {
|
||||
#[arg(short = 'k', long)]
|
||||
private_key: String,
|
||||
/// Ingress interface name (e.g. ng-ext)
|
||||
#[arg(long)]
|
||||
ingress: String,
|
||||
/// Egress interface name (e.g. ng-int)
|
||||
#[arg(long)]
|
||||
egress: String,
|
||||
/// Expiry date (YYYY-MM-DD)
|
||||
#[arg(short, long)]
|
||||
expires: String,
|
||||
/// Comma-separated list of features
|
||||
#[arg(short, long, default_value = "")]
|
||||
features: String,
|
||||
/// Output license file path
|
||||
#[arg(short, long, default_value = "license.key")]
|
||||
output: String,
|
||||
},
|
||||
/// Verify a license file
|
||||
Verify {
|
||||
#[arg(short = 'k', long)]
|
||||
public_key: String,
|
||||
#[arg(short, long)]
|
||||
license: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct LicensePayload {
|
||||
ingress_mac: String,
|
||||
egress_mac: String,
|
||||
expires: String,
|
||||
features: Vec<String>,
|
||||
}
|
||||
|
||||
fn get_mac(ifname: &str) -> String {
|
||||
for iface in datalink::interfaces() {
|
||||
if iface.name == ifname {
|
||||
if let Some(mac) = iface.mac {
|
||||
return format!(
|
||||
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
||||
mac.0, mac.1, mac.2, mac.3, mac.4, mac.5
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("Interface '{}' not found or has no MAC address", ifname);
|
||||
eprintln!("Available interfaces:");
|
||||
for iface in datalink::interfaces() {
|
||||
if let Some(mac) = iface.mac {
|
||||
eprintln!(" {} — {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
||||
iface.name, mac.0, mac.1, mac.2, mac.3, mac.4, mac.5);
|
||||
}
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Keygen { prefix } => keygen(&prefix),
|
||||
Commands::Issue { private_key, ingress, egress, expires, features, output } => {
|
||||
issue(&private_key, &ingress, &egress, &expires, &features, &output)
|
||||
}
|
||||
Commands::Verify { public_key, license } => verify(&public_key, &license),
|
||||
}
|
||||
}
|
||||
|
||||
fn keygen(prefix: &str) {
|
||||
let mut csprng = OsRng;
|
||||
let signing_key = SigningKey::generate(&mut csprng);
|
||||
let verifying_key = signing_key.verifying_key();
|
||||
|
||||
let priv_hex = hex_encode(signing_key.as_bytes());
|
||||
let pub_hex = hex_encode(verifying_key.as_bytes());
|
||||
|
||||
let priv_path = format!("{}_priv.key", prefix);
|
||||
let pub_path = format!("{}_pub.key", prefix);
|
||||
|
||||
fs::write(&priv_path, &priv_hex).expect("Failed to write private key");
|
||||
fs::write(&pub_path, &pub_hex).expect("Failed to write public key");
|
||||
|
||||
println!("Keypair generated:");
|
||||
println!(" Private key: {}", priv_path);
|
||||
println!(" Public key: {}", pub_path);
|
||||
println!();
|
||||
println!("Public key hex (embed in validator.rs):");
|
||||
println!(" {}", pub_hex);
|
||||
}
|
||||
|
||||
fn issue(private_key_path: &str, ingress: &str, egress: &str, expires: &str, features: &str, output: &str) {
|
||||
let ingress_mac = get_mac(ingress);
|
||||
let egress_mac = get_mac(egress);
|
||||
|
||||
println!("Detected MACs:");
|
||||
println!(" {} — {}", ingress, ingress_mac);
|
||||
println!(" {} — {}", egress, egress_mac);
|
||||
|
||||
let priv_hex = fs::read_to_string(private_key_path)
|
||||
.expect("Failed to read private key")
|
||||
.trim()
|
||||
.to_string();
|
||||
let priv_bytes = hex_decode(&priv_hex).expect("Invalid hex");
|
||||
let priv_array: [u8; 32] = priv_bytes.try_into().expect("Key must be 32 bytes");
|
||||
let signing_key = SigningKey::from_bytes(&priv_array);
|
||||
|
||||
let feature_list: Vec<String> = if features.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
features.split(',').map(|s| s.trim().to_string()).collect()
|
||||
};
|
||||
|
||||
let payload = LicensePayload {
|
||||
ingress_mac: ingress_mac.clone(),
|
||||
egress_mac: egress_mac.clone(),
|
||||
expires: expires.to_string(),
|
||||
features: feature_list,
|
||||
};
|
||||
|
||||
let payload_json = serde_json::to_string(&payload).expect("Failed to serialize");
|
||||
let payload_b64 = BASE64.encode(payload_json.as_bytes());
|
||||
let signature: Signature = signing_key.sign(payload_b64.as_bytes());
|
||||
let sig_b64 = BASE64.encode(signature.to_bytes());
|
||||
|
||||
let license_content = format!("{}.{}", payload_b64, sig_b64);
|
||||
fs::write(output, &license_content).expect("Failed to write license");
|
||||
|
||||
println!();
|
||||
println!("License issued:");
|
||||
println!(" Ingress MAC: {}", ingress_mac);
|
||||
println!(" Egress MAC: {}", egress_mac);
|
||||
println!(" Expires: {}", expires);
|
||||
println!(" Features: {:?}", payload.features);
|
||||
println!(" Output: {}", output);
|
||||
}
|
||||
|
||||
fn verify(public_key_path: &str, license_path: &str) {
|
||||
let pub_hex = fs::read_to_string(public_key_path)
|
||||
.expect("Failed to read public key")
|
||||
.trim()
|
||||
.to_string();
|
||||
let pub_bytes = hex_decode(&pub_hex).expect("Invalid hex");
|
||||
let pub_array: [u8; 32] = pub_bytes.try_into().expect("Key must be 32 bytes");
|
||||
let verifying_key = VerifyingKey::from_bytes(&pub_array).expect("Invalid public key");
|
||||
|
||||
let contents = fs::read_to_string(license_path)
|
||||
.expect("Failed to read license")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let parts: Vec<&str> = contents.splitn(2, '.').collect();
|
||||
if parts.len() != 2 {
|
||||
eprintln!("Invalid license format");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let sig_bytes = BASE64.decode(parts[1]).expect("Invalid signature");
|
||||
let sig_array: [u8; 64] = sig_bytes.try_into().expect("Signature must be 64 bytes");
|
||||
let signature = Signature::from_bytes(&sig_array);
|
||||
|
||||
match verifying_key.verify(parts[0].as_bytes(), &signature) {
|
||||
Ok(()) => {
|
||||
let payload_bytes = BASE64.decode(parts[0]).expect("Invalid payload");
|
||||
let payload: LicensePayload = serde_json::from_slice(&payload_bytes).expect("Invalid JSON");
|
||||
println!("License VALID:");
|
||||
println!(" Ingress MAC: {}", payload.ingress_mac);
|
||||
println!(" Egress MAC: {}", payload.egress_mac);
|
||||
println!(" Expires: {}", payload.expires);
|
||||
println!(" Features: {:?}", payload.features);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("License INVALID: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_encode(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
|
||||
fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
|
||||
if hex.len() % 2 != 0 {
|
||||
return Err("Odd-length hex string".to_string());
|
||||
}
|
||||
(0..hex.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| e.to_string()))
|
||||
.collect()
|
||||
}
|
||||
@ -54,6 +54,12 @@ rusqlite = { workspace = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
argon2 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
ed25519-dalek = { workspace = true, optional = true }
|
||||
base64 = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
license = ["dep:ed25519-dalek", "dep:base64"]
|
||||
|
||||
[build-dependencies]
|
||||
cargo_metadata = { workspace = true }
|
||||
|
||||
@ -11,6 +11,38 @@ fn main() {
|
||||
build_ebpf_package("ingress-ebpf", "ingress-ebpf");
|
||||
build_ebpf_package("egress-ebpf", "egress-ebpf");
|
||||
build_frontend();
|
||||
embed_license_public_key();
|
||||
}
|
||||
|
||||
fn embed_license_public_key() {
|
||||
// Only needed when license feature is enabled
|
||||
let license_enabled = env::var("CARGO_FEATURE_LICENSE").is_ok();
|
||||
|
||||
let project_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
let key_path = project_root.join("license_pub.key");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", key_path.display());
|
||||
|
||||
if key_path.exists() {
|
||||
let key_hex = fs::read_to_string(&key_path)
|
||||
.expect("Failed to read license_pub.key")
|
||||
.trim()
|
||||
.to_string();
|
||||
println!("cargo:rustc-env=LICENSE_PUBLIC_KEY={}", key_hex);
|
||||
} else if license_enabled {
|
||||
panic!(
|
||||
"license feature enabled but license_pub.key not found at {}.\n\
|
||||
Generate it with: cd license-generator && cargo run -- keygen\n\
|
||||
Then copy license_pub.key to the repo root.",
|
||||
key_path.display()
|
||||
);
|
||||
} else {
|
||||
// Feature not enabled, set clearly invalid placeholder (won't be used)
|
||||
println!("cargo:rustc-env=LICENSE_PUBLIC_KEY=DISABLED");
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ebpf_package(package_name: &str, target_subdir: &str) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use jsonwebtoken::{decode, encode, errors::ErrorKind, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::database::Database;
|
||||
@ -60,10 +60,9 @@ impl JwtService {
|
||||
pub fn validate_token(&self, token: &str) -> Result<Claims, Error> {
|
||||
let token_data = decode::<Claims>(token, &self.decoding_key, &Validation::default())
|
||||
.map_err(|e| {
|
||||
if e.to_string().contains("ExpiredSignature") {
|
||||
Error::from(AuthError::TokenExpired)
|
||||
} else {
|
||||
Error::from(AuthError::InvalidToken)
|
||||
match e.kind() {
|
||||
ErrorKind::ExpiredSignature => Error::from(AuthError::TokenExpired),
|
||||
_ => Error::from(AuthError::InvalidToken),
|
||||
}
|
||||
})?;
|
||||
Ok(token_data.claims)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use argon2::password_hash::rand_core::OsRng;
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::model::error::auth::AuthError;
|
||||
use crate::model::error::Error;
|
||||
|
||||
@ -221,7 +221,7 @@ impl Database {
|
||||
conn.execute(
|
||||
"INSERT INTO users (username, password_hash, role) VALUES (?1, ?2, ?3)",
|
||||
params![username, password_hash, role],
|
||||
).map_err(|e| {
|
||||
).map_err(|e| -> Error {
|
||||
if e.to_string().contains("UNIQUE constraint") {
|
||||
DatabaseError::UserAlreadyExists { username: username.to_string() }.into()
|
||||
} else {
|
||||
@ -234,6 +234,6 @@ impl Database {
|
||||
pub fn user_count(&self) -> Result<i64, Error> {
|
||||
let conn = self.conn.lock();
|
||||
conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))
|
||||
.map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() }.into())
|
||||
.map_err(|e| -> Error { DatabaseError::QueryFailed { reason: e.to_string() }.into() })
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ use aya::maps::lpm_trie::{Key, LpmTrie};
|
||||
use aya::maps::MapData;
|
||||
use aya::Ebpf;
|
||||
use ipnetwork::IpNetwork;
|
||||
use macros::log;
|
||||
use maxminddb::{geoip2, Reader};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
@ -13,7 +12,6 @@ use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::system::SystemLog;
|
||||
|
||||
/// Pre-indexed GeoIP prefix table, built once at startup.
|
||||
struct GeoIndex {
|
||||
|
||||
3
net-guardia/src/core/license/mod.rs
Normal file
3
net-guardia/src/core/license/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod validator;
|
||||
|
||||
pub use validator::LicenseInfo;
|
||||
235
net-guardia/src/core/license/validator.rs
Normal file
235
net-guardia/src/core/license/validator.rs
Normal file
@ -0,0 +1,235 @@
|
||||
use std::path::Path;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use ed25519_dalek::{Signature, VerifyingKey, Verifier};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::error::license::LicenseError;
|
||||
|
||||
/// Public key auto-embedded from license_pub.key at compile time.
|
||||
/// Generate with: cd license-generator && cargo run -- keygen
|
||||
/// Then place license_pub.key in the repo root.
|
||||
const PUBLIC_KEY_HEX: &str = env!("LICENSE_PUBLIC_KEY");
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LicensePayload {
|
||||
pub ingress_mac: String,
|
||||
pub egress_mac: String,
|
||||
pub expires: String,
|
||||
pub features: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LicenseInfo {
|
||||
pub payload: Option<LicensePayload>,
|
||||
pub valid: bool,
|
||||
pub days_remaining: i64,
|
||||
}
|
||||
|
||||
impl LicenseInfo {
|
||||
pub fn unlicensed() -> Self {
|
||||
Self {
|
||||
payload: None,
|
||||
valid: false,
|
||||
days_remaining: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_license(license_path: &str, ingress_ifname: &str, egress_ifname: &str) -> Result<LicenseInfo, crate::model::error::Error> {
|
||||
// Guard against builds where the license feature was not configured
|
||||
if PUBLIC_KEY_HEX == "DISABLED" {
|
||||
return Err(LicenseError::ValidationFailed {
|
||||
reason: "License validation not configured".to_string(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
// If path is empty, license is optional — return unlicensed
|
||||
if license_path.is_empty() {
|
||||
tracing::warn!("No license file configured — running without license");
|
||||
return Ok(LicenseInfo::unlicensed());
|
||||
}
|
||||
|
||||
let path = Path::new(license_path);
|
||||
if !path.exists() {
|
||||
tracing::warn!("License file '{}' not found — running without license", license_path);
|
||||
return Ok(LicenseInfo::unlicensed());
|
||||
}
|
||||
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.map_err(|_| LicenseError::FileNotFound { path: license_path.to_string() })?;
|
||||
|
||||
let contents = contents.trim();
|
||||
|
||||
// Format: base64(json_payload).base64(ed25519_signature)
|
||||
let parts: Vec<&str> = contents.splitn(2, '.').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(LicenseError::ValidationFailed {
|
||||
reason: "Invalid license format: expected <payload>.<signature>".to_string(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
let payload_b64 = parts[0];
|
||||
let signature_b64 = parts[1];
|
||||
|
||||
// Decode payload
|
||||
let payload_bytes = BASE64.decode(payload_b64)
|
||||
.map_err(|e| LicenseError::ValidationFailed {
|
||||
reason: format!("Failed to decode payload: {}", e),
|
||||
})?;
|
||||
|
||||
// Decode signature
|
||||
let sig_bytes = BASE64.decode(signature_b64)
|
||||
.map_err(|e| LicenseError::ValidationFailed {
|
||||
reason: format!("Failed to decode signature: {}", e),
|
||||
})?;
|
||||
|
||||
// Parse public key
|
||||
let pub_key_bytes = hex_decode(PUBLIC_KEY_HEX)
|
||||
.map_err(|e| LicenseError::ValidationFailed {
|
||||
reason: format!("Invalid embedded public key: {}", e),
|
||||
})?;
|
||||
|
||||
let pub_key_array: [u8; 32] = pub_key_bytes.try_into()
|
||||
.map_err(|_| LicenseError::ValidationFailed {
|
||||
reason: "Public key must be 32 bytes".to_string(),
|
||||
})?;
|
||||
|
||||
let verifying_key = VerifyingKey::from_bytes(&pub_key_array)
|
||||
.map_err(|_| LicenseError::ValidationFailed {
|
||||
reason: "Invalid public key".to_string(),
|
||||
})?;
|
||||
|
||||
// Parse signature
|
||||
let sig_array: [u8; 64] = sig_bytes.try_into()
|
||||
.map_err(|_| LicenseError::ValidationFailed {
|
||||
reason: "Signature must be 64 bytes".to_string(),
|
||||
})?;
|
||||
|
||||
let signature = Signature::from_bytes(&sig_array);
|
||||
|
||||
// Verify signature over the raw base64-encoded payload (not decoded bytes)
|
||||
verifying_key.verify(payload_b64.as_bytes(), &signature)
|
||||
.map_err(|_| LicenseError::InvalidSignature)?;
|
||||
|
||||
// Parse payload JSON
|
||||
let payload: LicensePayload = serde_json::from_slice(&payload_bytes)
|
||||
.map_err(|e| LicenseError::ValidationFailed {
|
||||
reason: format!("Failed to parse license payload: {}", e),
|
||||
})?;
|
||||
|
||||
// Verify NIC MAC addresses
|
||||
let actual_ingress_mac = get_interface_mac(ingress_ifname).unwrap_or_default();
|
||||
let actual_egress_mac = get_interface_mac(egress_ifname).unwrap_or_default();
|
||||
|
||||
if actual_ingress_mac != payload.ingress_mac {
|
||||
return Err(LicenseError::ValidationFailed {
|
||||
reason: "Ingress MAC mismatch — license not bound to this device".to_string(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
if actual_egress_mac != payload.egress_mac {
|
||||
return Err(LicenseError::ValidationFailed {
|
||||
reason: "Egress MAC mismatch — license not bound to this device".to_string(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
// Check expiry
|
||||
let today = chrono_free_today();
|
||||
let days_remaining = days_until(&payload.expires, &today)
|
||||
.map_err(|e| LicenseError::ValidationFailed {
|
||||
reason: format!("Invalid expiry date: {}", e),
|
||||
})?;
|
||||
|
||||
if days_remaining < 0 {
|
||||
return Err(LicenseError::Expired.into());
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"License valid — ingress={}, egress={}, expires={}, days_remaining={}, features={:?}",
|
||||
payload.ingress_mac, payload.egress_mac, payload.expires, days_remaining, payload.features
|
||||
);
|
||||
|
||||
Ok(LicenseInfo {
|
||||
payload: Some(payload),
|
||||
valid: true,
|
||||
days_remaining,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read MAC address from /sys/class/net/<ifname>/address (Linux only).
|
||||
fn get_interface_mac(ifname: &str) -> Option<String> {
|
||||
// Prevent path traversal
|
||||
if !ifname.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
|
||||
return None;
|
||||
}
|
||||
let path = format!("/sys/class/net/{}/address", ifname);
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_lowercase())
|
||||
}
|
||||
|
||||
/// Simple hex decoder without external dependency.
|
||||
fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
|
||||
if hex.len() % 2 != 0 {
|
||||
return Err("Odd-length hex string".to_string());
|
||||
}
|
||||
(0..hex.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| e.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse YYYY-MM-DD date and return days until expiry (no chrono dependency).
|
||||
fn chrono_free_today() -> (i32, u32, u32) {
|
||||
// Use UNIX_EPOCH to get today's date
|
||||
let secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
let days_since_epoch = (secs / 86400) as i32;
|
||||
epoch_days_to_ymd(days_since_epoch)
|
||||
}
|
||||
|
||||
fn parse_date(s: &str) -> Result<(i32, u32, u32), String> {
|
||||
let parts: Vec<&str> = s.split('-').collect();
|
||||
if parts.len() != 3 {
|
||||
return Err("Expected YYYY-MM-DD".to_string());
|
||||
}
|
||||
let y = parts[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
let m = parts[1].parse::<u32>().map_err(|e| e.to_string())?;
|
||||
let d = parts[2].parse::<u32>().map_err(|e| e.to_string())?;
|
||||
Ok((y, m, d))
|
||||
}
|
||||
|
||||
fn ymd_to_epoch_days(y: i32, m: u32, d: u32) -> i32 {
|
||||
// Algorithm from Howard Hinnant
|
||||
let y = if m <= 2 { y - 1 } else { y };
|
||||
let era = if y >= 0 { y } else { y - 399 } / 400;
|
||||
let yoe = (y - era * 400) as u32;
|
||||
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
|
||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
era * 146097 + doe as i32 - 719468
|
||||
}
|
||||
|
||||
fn epoch_days_to_ymd(days: i32) -> (i32, u32, u32) {
|
||||
let z = days + 719468;
|
||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||
let doe = (z - era * 146097) as u32;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe as i32 + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = if m <= 2 { y + 1 } else { y };
|
||||
(y, m, d)
|
||||
}
|
||||
|
||||
fn days_until(expiry_str: &str, today: &(i32, u32, u32)) -> Result<i64, String> {
|
||||
let (ey, em, ed) = parse_date(expiry_str)?;
|
||||
let expiry_days = ymd_to_epoch_days(ey, em, ed) as i64;
|
||||
let today_days = ymd_to_epoch_days(today.0, today.1, today.2) as i64;
|
||||
Ok(expiry_days - today_days)
|
||||
}
|
||||
@ -30,7 +30,6 @@ pub struct Engine {
|
||||
min_packets: usize,
|
||||
batch_size: usize,
|
||||
inference_interval_secs: u64,
|
||||
flow_timeout_us: u64,
|
||||
traffic_logger: Option<Arc<TrafficLogger>>,
|
||||
}
|
||||
|
||||
@ -61,7 +60,6 @@ impl Engine {
|
||||
min_packets: engine_config.min_packets,
|
||||
batch_size: engine_config.batch_size,
|
||||
inference_interval_secs: engine_config.inference_interval_secs,
|
||||
flow_timeout_us: engine_config.flow_timeout_us,
|
||||
traffic_logger,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,5 +2,7 @@ pub mod auth;
|
||||
pub mod database;
|
||||
pub mod ebpf;
|
||||
pub mod infrastructure;
|
||||
#[cfg(feature = "license")]
|
||||
pub mod license;
|
||||
pub mod ml;
|
||||
pub mod system;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::route;
|
||||
@ -16,11 +17,17 @@ use crate::core::database::Database;
|
||||
use crate::core::ebpf::EbpfServices;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::infrastructure::MLService;
|
||||
#[cfg(feature = "license")]
|
||||
use crate::core::license::LicenseInfo;
|
||||
#[cfg(feature = "license")]
|
||||
use crate::core::license::validator::validate_license;
|
||||
use crate::core::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::direction::FlowDirection;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::list_type::ListType;
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::log::system::SystemLog;
|
||||
use crate::utils::logging::Logging;
|
||||
@ -42,6 +49,8 @@ pub struct System {
|
||||
pub app_services: Arc<MLService>,
|
||||
pub db: Arc<Database>,
|
||||
pub jwt_service: Arc<JwtService>,
|
||||
#[cfg(feature = "license")]
|
||||
pub license_info: Arc<LicenseInfo>,
|
||||
pub ingress_ebpf: Ebpf,
|
||||
pub egress_ebpf: Ebpf,
|
||||
#[allow(dead_code)]
|
||||
@ -54,6 +63,13 @@ impl System {
|
||||
let mut egress_ebpf = Self::load_ebpf("egress")?;
|
||||
let app_config = Arc::new(AppConfig::new()?);
|
||||
|
||||
#[cfg(feature = "license")]
|
||||
let license_info = Arc::new(validate_license(
|
||||
&app_config.misc.license_file,
|
||||
&app_config.network.ingress_ifname,
|
||||
&app_config.network.egress_ifname,
|
||||
)?);
|
||||
|
||||
let ingress_program_array = Self::configure_ingress_pipeline(
|
||||
&mut ingress_ebpf,
|
||||
&app_config.pipeline.ingress,
|
||||
@ -128,6 +144,61 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
// Load persisted ACL rules
|
||||
if let Ok(rules) = db.load_acl_rules() {
|
||||
let mut restored = 0u32;
|
||||
for (ip_version, direction, list_type, ip_address, port) in &rules {
|
||||
let dir = match direction.as_str() {
|
||||
"source" => FlowDirection::Source,
|
||||
"destination" => FlowDirection::Destination,
|
||||
other => {
|
||||
tracing::warn!("Unknown ACL direction '{}', skipping", other);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let lt = match list_type.as_str() {
|
||||
"whitelist" => ListType::White,
|
||||
"blacklist" => ListType::Black,
|
||||
other => {
|
||||
tracing::warn!("Unknown ACL list type '{}', skipping", other);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let result = match ip_version {
|
||||
4 => {
|
||||
match ip_address.parse::<Ipv4Addr>() {
|
||||
Ok(addr) => ebpf_services.access_control.add_ipv4_list(dir, lt, SocketAddrV4::new(addr, *port)).await,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to parse IPv4 address '{}': {}", ip_address, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
match ip_address.parse::<Ipv6Addr>() {
|
||||
Ok(addr) => ebpf_services.access_control.add_ipv6_list(dir, lt, SocketAddrV6::new(addr, *port, 0, 0)).await,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to parse IPv6 address '{}': {}", ip_address, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
other => {
|
||||
tracing::warn!("Unknown IP version {}, skipping", other);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(e) = result {
|
||||
tracing::warn!("Failed to restore ACL rule ({} {} {}:{}): {}", direction, list_type, ip_address, port, e);
|
||||
} else {
|
||||
restored += 1;
|
||||
}
|
||||
}
|
||||
if restored > 0 {
|
||||
tracing::info!("Restored {} ACL rules from database", restored);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(System {
|
||||
app_config,
|
||||
inference_config,
|
||||
@ -135,6 +206,8 @@ impl System {
|
||||
app_services,
|
||||
db,
|
||||
jwt_service,
|
||||
#[cfg(feature = "license")]
|
||||
license_info,
|
||||
ingress_ebpf,
|
||||
egress_ebpf,
|
||||
ingress_program_array,
|
||||
@ -225,6 +298,8 @@ impl System {
|
||||
let drop_monitor = self.ebpf_services.drop_monitor.clone();
|
||||
let db = self.db.clone();
|
||||
let jwt_service = self.jwt_service.clone();
|
||||
#[cfg(feature = "license")]
|
||||
let license_info = self.license_info.clone();
|
||||
let port = self.app_config.http.http_server_bind_port;
|
||||
HttpServer::new(move || {
|
||||
let cors = actix_cors::Cors::default()
|
||||
@ -233,7 +308,7 @@ impl System {
|
||||
.allow_any_method()
|
||||
.allow_any_header()
|
||||
.max_age(3600);
|
||||
App::new()
|
||||
let app = App::new()
|
||||
.wrap(cors)
|
||||
.app_data(web::Data::from(app_config.clone()))
|
||||
.app_data(web::Data::from(inference_config.clone()))
|
||||
@ -248,8 +323,10 @@ impl System {
|
||||
.app_data(web::Data::from(flow_statistics.clone()))
|
||||
.app_data(web::Data::from(drop_monitor.clone()))
|
||||
.app_data(web::Data::from(db.clone()))
|
||||
.app_data(web::Data::from(jwt_service.clone()))
|
||||
.service(
|
||||
.app_data(web::Data::from(jwt_service.clone()));
|
||||
#[cfg(feature = "license")]
|
||||
let app = app.app_data(web::Data::from(license_info.clone()));
|
||||
app.service(
|
||||
web::scope("/api")
|
||||
.wrap(crate::core::auth::middleware::AuthMiddleware)
|
||||
.service(auth::initialize())
|
||||
|
||||
@ -64,9 +64,12 @@ pub struct MiscConfig {
|
||||
pub geoip_db_name: String,
|
||||
#[serde(default = "default_db_path")]
|
||||
pub database_path: String,
|
||||
#[serde(default = "default_license_path")]
|
||||
pub license_file: String,
|
||||
}
|
||||
|
||||
fn default_db_path() -> String { "net-guardia.db".to_string() }
|
||||
fn default_license_path() -> String { "license.key".to_string() }
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PipelineConfig {
|
||||
|
||||
@ -2,18 +2,23 @@ use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
AuthError {
|
||||
#[no_source]
|
||||
#[error("Invalid credentials")]
|
||||
InvalidCredentials => tracing::Level::WARN,
|
||||
|
||||
#[no_source]
|
||||
#[error("Token expired")]
|
||||
TokenExpired => tracing::Level::WARN,
|
||||
|
||||
#[no_source]
|
||||
#[error("Invalid token")]
|
||||
InvalidToken => tracing::Level::WARN,
|
||||
|
||||
#[no_source]
|
||||
#[error("Insufficient permissions")]
|
||||
InsufficientPermissions => tracing::Level::WARN,
|
||||
|
||||
#[no_source]
|
||||
#[error("Missing authorization header")]
|
||||
MissingAuthHeader => tracing::Level::WARN,
|
||||
}
|
||||
|
||||
19
net-guardia/src/model/error/license.rs
Normal file
19
net-guardia/src/model/error/license.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
LicenseError {
|
||||
#[no_source]
|
||||
#[error("License file not found: {path}")]
|
||||
FileNotFound { path: String } => tracing::Level::WARN,
|
||||
|
||||
#[error("Invalid license signature")]
|
||||
InvalidSignature => tracing::Level::ERROR,
|
||||
|
||||
#[error("License has expired")]
|
||||
Expired => tracing::Level::WARN,
|
||||
|
||||
#[no_source]
|
||||
#[error("License validation failed: {reason}")]
|
||||
ValidationFailed { reason: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,8 @@ pub mod database;
|
||||
pub mod ebpf;
|
||||
pub mod http;
|
||||
pub mod io;
|
||||
#[cfg(feature = "license")]
|
||||
pub mod license;
|
||||
pub mod misc;
|
||||
pub mod ml;
|
||||
pub mod system;
|
||||
@ -14,6 +16,8 @@ use crate::model::error::database::DatabaseError;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::io::IOError;
|
||||
#[cfg(feature = "license")]
|
||||
use crate::model::error::license::LicenseError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::ml::MLError;
|
||||
use crate::model::error::system::SystemError;
|
||||
@ -32,6 +36,9 @@ pub enum Error {
|
||||
ML(MLError),
|
||||
#[error("{0}")]
|
||||
IO(IOError),
|
||||
#[cfg(feature = "license")]
|
||||
#[error("{0}")]
|
||||
License(LicenseError),
|
||||
#[error("{0}")]
|
||||
Misc(MiscError),
|
||||
#[error("{0}")]
|
||||
@ -68,6 +75,13 @@ impl From<IOError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "license")]
|
||||
impl From<LicenseError> for Error {
|
||||
fn from(error: LicenseError) -> Self {
|
||||
Self::License(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MiscError> for Error {
|
||||
fn from(error: MiscError) -> Self {
|
||||
Self::Misc(error)
|
||||
@ -84,4 +98,4 @@ impl From<MLError> for Error {
|
||||
fn from(error: MLError) -> Self {
|
||||
Self::ML(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub struct UserPacket {
|
||||
pub ip_version: u8,
|
||||
pub protocol: u8,
|
||||
@ -15,23 +13,3 @@ pub struct UserPacket {
|
||||
pub timestamp_us: u64,
|
||||
pub is_forward: bool,
|
||||
}
|
||||
|
||||
impl UserPacket {
|
||||
/// Format source IP as a human-readable string.
|
||||
pub fn src_ip_string(&self) -> String {
|
||||
Self::ip_bytes_to_string(self.ip_version, &self.src_ip)
|
||||
}
|
||||
|
||||
/// Format destination IP as a human-readable string.
|
||||
pub fn dst_ip_string(&self) -> String {
|
||||
Self::ip_bytes_to_string(self.ip_version, &self.dst_ip)
|
||||
}
|
||||
|
||||
fn ip_bytes_to_string(ip_version: u8, bytes: &[u8; 16]) -> String {
|
||||
if ip_version == 6 {
|
||||
Ipv6Addr::from(*bytes).to_string()
|
||||
} else {
|
||||
Ipv4Addr::from([bytes[0], bytes[1], bytes[2], bytes[3]]).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder, Scope};
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse, Responder, Scope};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::core::auth::jwt::{Claims, JwtService};
|
||||
|
||||
@ -1,10 +1,20 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Scope};
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/system")
|
||||
.route("/boot-time", web::get().to(get_boot_time))
|
||||
let scope = web::scope("/system")
|
||||
.route("/boot-time", web::get().to(get_boot_time));
|
||||
|
||||
#[cfg(feature = "license")]
|
||||
let scope = scope.route("/license", web::get().to(get_license_info));
|
||||
|
||||
scope
|
||||
}
|
||||
|
||||
async fn get_boot_time() -> impl Responder {
|
||||
HttpResponse::Ok().json(crate::utils::boot_time::boot_time())
|
||||
}
|
||||
|
||||
#[cfg(feature = "license")]
|
||||
async fn get_license_info(license_info: web::Data<crate::core::license::LicenseInfo>) -> impl Responder {
|
||||
HttpResponse::Ok().json(license_info.get_ref())
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user