From 5fbb32d243226ac24d8380ec5b9420d77d153739 Mon Sep 17 00:00:00 2001 From: ospab Date: Thu, 1 Jan 2026 18:54:36 +0300 Subject: [PATCH] start, reverse guard, cli-frontend for server and client --- .gitignore | 4 + Cargo.lock | 1614 ++++++++++++++++++++++++++++++++ Cargo.toml | 52 + oncp/Cargo.toml | 16 + oncp/src/billing.rs | 163 ++++ oncp/src/lib.rs | 5 + oncp/src/session.rs | 109 +++ osds/Cargo.toml | 13 + osds/src/dns.rs | 139 +++ osds/src/lib.rs | 3 + osn/Cargo.toml | 12 + osn/src/lib.rs | 3 + osn/src/tun.rs | 95 ++ ostp-client/Cargo.toml | 26 + ostp-client/src/main.rs | 420 +++++++++ ostp-guard/Cargo.toml | 14 + ostp-guard/src/anti_debug.rs | 228 +++++ ostp-guard/src/anti_vm.rs | 315 +++++++ ostp-guard/src/control_flow.rs | 231 +++++ ostp-guard/src/lib.rs | 61 ++ ostp-guard/src/obfuscate.rs | 156 +++ ostp-server/Cargo.toml | 23 + ostp-server/src/main.rs | 164 ++++ ostp/Cargo.toml | 17 + ostp/src/client.rs | 159 ++++ ostp/src/crypto.rs | 149 +++ ostp/src/lib.rs | 11 + ostp/src/mimicry.rs | 174 ++++ ostp/src/server.rs | 192 ++++ ostp/src/uot.rs | 132 +++ 30 files changed, 4700 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 oncp/Cargo.toml create mode 100644 oncp/src/billing.rs create mode 100644 oncp/src/lib.rs create mode 100644 oncp/src/session.rs create mode 100644 osds/Cargo.toml create mode 100644 osds/src/dns.rs create mode 100644 osds/src/lib.rs create mode 100644 osn/Cargo.toml create mode 100644 osn/src/lib.rs create mode 100644 osn/src/tun.rs create mode 100644 ostp-client/Cargo.toml create mode 100644 ostp-client/src/main.rs create mode 100644 ostp-guard/Cargo.toml create mode 100644 ostp-guard/src/anti_debug.rs create mode 100644 ostp-guard/src/anti_vm.rs create mode 100644 ostp-guard/src/control_flow.rs create mode 100644 ostp-guard/src/lib.rs create mode 100644 ostp-guard/src/obfuscate.rs create mode 100644 ostp-server/Cargo.toml create mode 100644 ostp-server/src/main.rs create mode 100644 ostp/Cargo.toml create mode 100644 ostp/src/client.rs create mode 100644 ostp/src/crypto.rs create mode 100644 ostp/src/lib.rs create mode 100644 ostp/src/mimicry.rs create mode 100644 ostp/src/server.rs create mode 100644 ostp/src/uot.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c76fa9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.cargo +/.github +/prompt.md +/target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0f69bc7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1614 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "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", + "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", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "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 = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oncp" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "ostp", + "rusqlite", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "osds" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "ostp", + "rand", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "osn" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "ostp" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chacha20poly1305", + "hmac", + "rand", + "sha2", + "thiserror 2.0.17", + "tokio", + "tracing", + "uuid", + "x25519-dalek", +] + +[[package]] +name = "ostp-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "console", + "dialoguer", + "hex", + "ostp", + "ostp-guard", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ostp-guard" +version = "0.1.0" +dependencies = [ + "libc", + "rand", + "winapi", +] + +[[package]] +name = "ostp-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "hex", + "oncp", + "ostp", + "ostp-guard", + "rand", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ed5f8cd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,52 @@ +[workspace] +resolver = "2" +members = ["ostp", "oncp", "osn", "osds", "ostp-server", "ostp-client", "ostp-guard"] + +[workspace.package] +version = "0.1.0" +edition = "2024" +authors = ["Ospab Team"] +license = "Proprietary" + +# ============================================================================ +# HARDENED RELEASE PROFILE - Anti-Reverse Engineering +# ============================================================================ +[profile.release] +lto = true # Link-Time Optimization - merges all code, removes boundaries +codegen-units = 1 # Single codegen unit - better optimization, harder to analyze +panic = "abort" # No unwinding info - smaller binary, no stack traces +strip = "symbols" # Remove ALL symbols - no function names in binary +opt-level = 3 # Maximum optimization +debug = false # No debug info +debug-assertions = false # No debug assertions +overflow-checks = false # No overflow checks (performance + less obvious control flow) +incremental = false # Reproducible builds + +# Distribution build - even more aggressive +[profile.dist] +inherits = "release" +lto = "fat" # Full LTO across all crates +strip = "debuginfo" # Alternative stripping + +[workspace.dependencies] +tokio = { version = "1.40", features = ["full"] } +ring = "0.17" +chacha20poly1305 = "0.10" +x25519-dalek = { version = "2.0", features = ["static_secrets"] } +bytes = "1.7" +rusqlite = { version = "0.32", features = ["bundled"] } +anyhow = "1.0" +thiserror = "2.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.10", features = ["v4", "serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +hmac = "0.12" +sha2 = "0.10" +rand = "0.8" +chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4.5", features = ["derive", "env"] } +hex = "0.4" +dialoguer = "0.11" +console = "0.15" diff --git a/oncp/Cargo.toml b/oncp/Cargo.toml new file mode 100644 index 0000000..e3ac44c --- /dev/null +++ b/oncp/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "oncp" +version.workspace = true +edition.workspace = true + +[dependencies] +tokio.workspace = true +rusqlite.workspace = true +anyhow.workspace = true +thiserror.workspace = true +tracing.workspace = true +uuid.workspace = true +serde.workspace = true +serde_json.workspace = true +chrono.workspace = true +ostp = { path = "../ostp" } diff --git a/oncp/src/billing.rs b/oncp/src/billing.rs new file mode 100644 index 0000000..d81f040 --- /dev/null +++ b/oncp/src/billing.rs @@ -0,0 +1,163 @@ +//! User billing and quota management + +use chrono::{DateTime, Utc}; +use std::sync::Mutex; +use thiserror::Error; +use uuid::Uuid; + +#[derive(Error, Debug)] +pub enum BillingError { + #[error("user not found: {0}")] + UserNotFound(Uuid), + #[error("subscription expired")] + Expired, + #[error("bandwidth quota exceeded")] + QuotaExceeded, + #[error("database error: {0}")] + Database(#[from] rusqlite::Error), + #[error("lock error")] + LockError, +} + +/// User account with billing info +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct User { + pub uuid: Uuid, + pub bandwidth_quota: u64, // bytes allowed + pub bandwidth_used: u64, // bytes used + pub expires_at: DateTime, // subscription expiry + pub active: bool, +} + +impl User { + pub fn new(quota_gb: u64, valid_days: i64) -> Self { + Self { + uuid: Uuid::new_v4(), + bandwidth_quota: quota_gb * 1024 * 1024 * 1024, + bandwidth_used: 0, + expires_at: Utc::now() + chrono::Duration::days(valid_days), + active: true, + } + } + + pub fn is_valid(&self) -> bool { + self.active && Utc::now() < self.expires_at && self.bandwidth_used < self.bandwidth_quota + } + + pub fn remaining_quota(&self) -> u64 { + self.bandwidth_quota.saturating_sub(self.bandwidth_used) + } +} + +/// Abstract user registry trait +pub trait UserRegistry: Send + Sync { + fn get_user(&self, uuid: &Uuid) -> Result, BillingError>; + fn create_user(&self, user: &User) -> Result<(), BillingError>; + fn update_bandwidth(&self, uuid: &Uuid, bytes: u64) -> Result<(), BillingError>; + fn validate_user(&self, uuid: &Uuid) -> Result; +} + +/// SQLite implementation (thread-safe via Mutex) +pub struct SqliteRegistry { + conn: Mutex, +} + +impl SqliteRegistry { + pub fn new(path: &str) -> Result { + let conn = rusqlite::Connection::open(path)?; + conn.execute( + "CREATE TABLE IF NOT EXISTS users ( + uuid TEXT PRIMARY KEY, + bandwidth_quota INTEGER NOT NULL, + bandwidth_used INTEGER NOT NULL DEFAULT 0, + expires_at TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 1 + )", + [], + )?; + Ok(Self { conn: Mutex::new(conn) }) + } + + pub fn in_memory() -> Result { + Self::new(":memory:") + } +} + +impl UserRegistry for SqliteRegistry { + fn get_user(&self, uuid: &Uuid) -> Result, BillingError> { + let conn = self.conn.lock().map_err(|_| BillingError::LockError)?; + let mut stmt = conn + .prepare("SELECT uuid, bandwidth_quota, bandwidth_used, expires_at, active FROM users WHERE uuid = ?")?; + + let mut rows = stmt.query([uuid.to_string()])?; + + if let Some(row) = rows.next()? { + let uuid_str: String = row.get(0)?; + let expires_str: String = row.get(3)?; + Ok(Some(User { + uuid: Uuid::parse_str(&uuid_str).unwrap(), + bandwidth_quota: row.get(1)?, + bandwidth_used: row.get(2)?, + expires_at: DateTime::parse_from_rfc3339(&expires_str) + .unwrap() + .with_timezone(&Utc), + active: row.get::<_, i32>(4)? == 1, + })) + } else { + Ok(None) + } + } + + fn create_user(&self, user: &User) -> Result<(), BillingError> { + let conn = self.conn.lock().map_err(|_| BillingError::LockError)?; + conn.execute( + "INSERT INTO users (uuid, bandwidth_quota, bandwidth_used, expires_at, active) VALUES (?, ?, ?, ?, ?)", + ( + user.uuid.to_string(), + user.bandwidth_quota, + user.bandwidth_used, + user.expires_at.to_rfc3339(), + if user.active { 1 } else { 0 }, + ), + )?; + Ok(()) + } + + fn update_bandwidth(&self, uuid: &Uuid, bytes: u64) -> Result<(), BillingError> { + let conn = self.conn.lock().map_err(|_| BillingError::LockError)?; + let updated = conn.execute( + "UPDATE users SET bandwidth_used = bandwidth_used + ? WHERE uuid = ?", + (bytes, uuid.to_string()), + )?; + if updated == 0 { + return Err(BillingError::UserNotFound(*uuid)); + } + Ok(()) + } + + fn validate_user(&self, uuid: &Uuid) -> Result { + match self.get_user(uuid)? { + Some(user) => Ok(user.is_valid()), + None => Ok(false), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_user_lifecycle() { + let reg = SqliteRegistry::in_memory().unwrap(); + let user = User::new(100, 30); // 100GB, 30 days + let uuid = user.uuid; + + reg.create_user(&user).unwrap(); + assert!(reg.validate_user(&uuid).unwrap()); + + reg.update_bandwidth(&uuid, 1024).unwrap(); + let updated = reg.get_user(&uuid).unwrap().unwrap(); + assert_eq!(updated.bandwidth_used, 1024); + } +} diff --git a/oncp/src/lib.rs b/oncp/src/lib.rs new file mode 100644 index 0000000..2d8777e --- /dev/null +++ b/oncp/src/lib.rs @@ -0,0 +1,5 @@ +pub mod billing; +pub mod session; + +pub use billing::{BillingError, SqliteRegistry, User, UserRegistry}; +pub use session::{Session, SessionManager}; diff --git a/oncp/src/session.rs b/oncp/src/session.rs new file mode 100644 index 0000000..aed4ba9 --- /dev/null +++ b/oncp/src/session.rs @@ -0,0 +1,109 @@ +//! Session management and heartbeat + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::RwLock; +use uuid::Uuid; + +/// Session state +#[derive(Debug, Clone)] +pub struct Session { + pub user_id: Uuid, + pub session_id: Uuid, + pub created_at: Instant, + pub last_heartbeat: Instant, + pub bytes_tx: u64, + pub bytes_rx: u64, + pub sni: String, +} + +impl Session { + pub fn new(user_id: Uuid, sni: String) -> Self { + let now = Instant::now(); + Self { + user_id, + session_id: Uuid::new_v4(), + created_at: now, + last_heartbeat: now, + bytes_tx: 0, + bytes_rx: 0, + sni, + } + } + + pub fn heartbeat(&mut self) { + self.last_heartbeat = Instant::now(); + } + + pub fn is_alive(&self, timeout: Duration) -> bool { + self.last_heartbeat.elapsed() < timeout + } +} + +/// Session manager +pub struct SessionManager { + sessions: Arc>>, + heartbeat_timeout: Duration, +} + +impl SessionManager { + pub fn new(heartbeat_timeout_secs: u64) -> Self { + Self { + sessions: Arc::new(RwLock::new(HashMap::new())), + heartbeat_timeout: Duration::from_secs(heartbeat_timeout_secs), + } + } + + pub async fn create_session(&self, user_id: Uuid, sni: String) -> Uuid { + let session = Session::new(user_id, sni); + let session_id = session.session_id; + self.sessions.write().await.insert(session_id, session); + session_id + } + + pub async fn heartbeat(&self, session_id: &Uuid) -> bool { + if let Some(session) = self.sessions.write().await.get_mut(session_id) { + session.heartbeat(); + true + } else { + false + } + } + + pub async fn get_session(&self, session_id: &Uuid) -> Option { + self.sessions.read().await.get(session_id).cloned() + } + + pub async fn remove_session(&self, session_id: &Uuid) -> Option { + self.sessions.write().await.remove(session_id) + } + + pub async fn cleanup_stale(&self) -> Vec { + let mut sessions = self.sessions.write().await; + let timeout = self.heartbeat_timeout; + let stale: Vec = sessions + .iter() + .filter(|(_, s)| !s.is_alive(timeout)) + .map(|(id, _)| *id) + .collect(); + + for id in &stale { + sessions.remove(id); + } + stale + } + + pub async fn update_traffic(&self, session_id: &Uuid, tx: u64, rx: u64) { + if let Some(session) = self.sessions.write().await.get_mut(session_id) { + session.bytes_tx += tx; + session.bytes_rx += rx; + } + } +} + +impl Default for SessionManager { + fn default() -> Self { + Self::new(60) + } +} diff --git a/osds/Cargo.toml b/osds/Cargo.toml new file mode 100644 index 0000000..c3b8f72 --- /dev/null +++ b/osds/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "osds" +version.workspace = true +edition.workspace = true + +[dependencies] +tokio.workspace = true +bytes.workspace = true +anyhow.workspace = true +thiserror.workspace = true +tracing.workspace = true +rand.workspace = true +ostp = { path = "../ostp" } diff --git a/osds/src/dns.rs b/osds/src/dns.rs new file mode 100644 index 0000000..0921ba4 --- /dev/null +++ b/osds/src/dns.rs @@ -0,0 +1,139 @@ +//! Stealth DNS resolver - DoH/DoT with anti-hijacking + +use bytes::{BufMut, BytesMut}; +use std::net::SocketAddr; +use thiserror::Error; +use tokio::net::UdpSocket; + +#[derive(Error, Debug)] +pub enum DnsError { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("invalid query")] + InvalidQuery, + #[error("upstream timeout")] + Timeout, + #[error("hijacking detected")] + HijackDetected, +} + +/// DNS query types +#[derive(Debug, Clone, Copy)] +pub enum QueryType { + A = 1, + AAAA = 28, + CNAME = 5, + TXT = 16, +} + +/// Minimal DNS query builder +pub fn build_dns_query(domain: &str, qtype: QueryType) -> BytesMut { + let mut buf = BytesMut::with_capacity(512); + + // Transaction ID (random) + let txid: u16 = rand::random(); + buf.put_u16(txid); + + // Flags: standard query, recursion desired + buf.put_u16(0x0100); + + // Questions: 1, Answers: 0, Authority: 0, Additional: 0 + buf.put_u16(1); + buf.put_u16(0); + buf.put_u16(0); + buf.put_u16(0); + + // QNAME + for label in domain.split('.') { + buf.put_u8(label.len() as u8); + buf.put_slice(label.as_bytes()); + } + buf.put_u8(0); // null terminator + + // QTYPE and QCLASS (IN) + buf.put_u16(qtype as u16); + buf.put_u16(1); // IN class + + buf +} + +/// DNS forwarder that tunnels queries through OSTP +pub struct StealthDnsForwarder { + listen_addr: SocketAddr, + /// Upstream resolver (will be tunneled through OSTP) + upstream: SocketAddr, +} + +impl StealthDnsForwarder { + pub fn new(listen: SocketAddr, upstream: SocketAddr) -> Self { + Self { + listen_addr: listen, + upstream, + } + } + + /// Start DNS listener (intercepts local queries) + pub async fn run(&self) -> Result<(), DnsError> { + let socket = UdpSocket::bind(self.listen_addr).await?; + tracing::info!("DNS forwarder listening on {}", self.listen_addr); + + let mut buf = [0u8; 512]; + loop { + let (len, src) = socket.recv_from(&mut buf).await?; + let query = &buf[..len]; + + // TODO: Encrypt and forward through OSTP tunnel instead of direct + // For now, direct forward (to be replaced with tunnel) + match self.forward_query(query).await { + Ok(response) => { + let _ = socket.send_to(&response, src).await; + } + Err(e) => { + tracing::warn!("DNS forward failed: {}", e); + } + } + } + } + + async fn forward_query(&self, query: &[u8]) -> Result, DnsError> { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + socket.send_to(query, self.upstream).await?; + + let mut response = vec![0u8; 512]; + let timeout = tokio::time::timeout( + std::time::Duration::from_secs(5), + socket.recv_from(&mut response), + ) + .await; + + match timeout { + Ok(Ok((len, _))) => { + response.truncate(len); + Ok(response) + } + Ok(Err(e)) => Err(DnsError::Io(e)), + Err(_) => Err(DnsError::Timeout), + } + } +} + +/// Anti-hijacking: verify response matches known-good resolver fingerprint +pub fn detect_hijack(response: &[u8], expected_patterns: &[[u8; 4]]) -> bool { + // Check if response contains known hijack IPs (ISP redirect pages, etc.) + // Simplified: check for common hijack patterns + if response.len() < 12 { + return true; // Suspicious short response + } + + // Check for NXDOMAIN being rewritten (common hijack) + let rcode = response[3] & 0x0F; + if rcode == 0 { + // NOERROR - check if IP is in known hijack list + for pattern in expected_patterns { + if response.windows(4).any(|w| w == pattern) { + return true; + } + } + } + false +} diff --git a/osds/src/lib.rs b/osds/src/lib.rs new file mode 100644 index 0000000..03f2e0e --- /dev/null +++ b/osds/src/lib.rs @@ -0,0 +1,3 @@ +pub mod dns; + +pub use dns::{build_dns_query, detect_hijack, DnsError, QueryType, StealthDnsForwarder}; diff --git a/osn/Cargo.toml b/osn/Cargo.toml new file mode 100644 index 0000000..60ef2ae --- /dev/null +++ b/osn/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "osn" +version.workspace = true +edition.workspace = true + +[dependencies] +tokio.workspace = true +bytes.workspace = true +anyhow.workspace = true +thiserror.workspace = true +tracing.workspace = true +async-trait = "0.1" diff --git a/osn/src/lib.rs b/osn/src/lib.rs new file mode 100644 index 0000000..d69c470 --- /dev/null +++ b/osn/src/lib.rs @@ -0,0 +1,3 @@ +pub mod tun; + +pub use tun::{DummyTun, Router, TunConfig, TunDevice, TunError}; diff --git a/osn/src/tun.rs b/osn/src/tun.rs new file mode 100644 index 0000000..21ad488 --- /dev/null +++ b/osn/src/tun.rs @@ -0,0 +1,95 @@ +//! Virtual network interface (TUN/TAP) abstraction + +use bytes::Bytes; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TunError { + #[error("failed to create interface: {0}")] + Create(String), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("interface not ready")] + NotReady, +} + +/// TUN device configuration +#[derive(Debug, Clone)] +pub struct TunConfig { + pub name: String, + pub address: [u8; 4], + pub netmask: [u8; 4], + pub mtu: u16, +} + +impl Default for TunConfig { + fn default() -> Self { + Self { + name: "ostp0".into(), + address: [10, 0, 0, 1], + netmask: [255, 255, 255, 0], + mtu: 1400, + } + } +} + +/// Abstract TUN device trait +#[async_trait::async_trait] +pub trait TunDevice: Send + Sync { + async fn read(&self) -> Result; + async fn write(&self, data: &[u8]) -> Result; + fn mtu(&self) -> u16; +} + +/// Placeholder TUN implementation (platform-specific impl needed) +pub struct DummyTun { + config: TunConfig, +} + +impl DummyTun { + pub fn new(config: TunConfig) -> Self { + Self { config } + } +} + +#[async_trait::async_trait] +impl TunDevice for DummyTun { + async fn read(&self) -> Result { + // Platform-specific: use wintun on Windows, tun-tap on Linux + Err(TunError::NotReady) + } + + async fn write(&self, _data: &[u8]) -> Result { + Err(TunError::NotReady) + } + + fn mtu(&self) -> u16 { + self.config.mtu + } +} + +/// IP packet routing helper +pub struct Router { + default_gateway: [u8; 4], +} + +impl Router { + pub fn new(gateway: [u8; 4]) -> Self { + Self { + default_gateway: gateway, + } + } + + /// Check if IP should be tunneled + pub fn should_tunnel(&self, dest_ip: &[u8; 4]) -> bool { + // Don't tunnel local/private ranges by default + !matches!( + dest_ip, + [10, ..] | [127, ..] | [192, 168, ..] | [172, 16..=31, ..] + ) + } + + pub fn gateway(&self) -> [u8; 4] { + self.default_gateway + } +} diff --git a/ostp-client/Cargo.toml b/ostp-client/Cargo.toml new file mode 100644 index 0000000..5a07d91 --- /dev/null +++ b/ostp-client/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ostp-client" +version.workspace = true +edition.workspace = true +description = "OSTP Stealth VPN Client" + +[[bin]] +name = "ostp-client" +path = "src/main.rs" + +[dependencies] +ostp = { path = "../ostp" } +ostp-guard = { path = "../ostp-guard" } +tokio.workspace = true +anyhow.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +clap.workspace = true +hex.workspace = true +serde.workspace = true +serde_json.workspace = true +dialoguer.workspace = true +console.workspace = true + +[target.'cfg(windows)'.dependencies] +# Windows-specific deps will go here (wintun, etc.) diff --git a/ostp-client/src/main.rs b/ostp-client/src/main.rs new file mode 100644 index 0000000..8ff2824 --- /dev/null +++ b/ostp-client/src/main.rs @@ -0,0 +1,420 @@ +//! OSTP Client CLI - Stealth VPN Client for Windows +//! +//! Usage: +//! ostp-client connect --server 1.2.3.4:8443 --psk +//! ostp-client --config %APPDATA%/ostp/client.json + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use console::{style, Emoji}; +use dialoguer::{Confirm, Input, Select}; +use ostp::{ClientConfig, OstpClient}; +use ostp_guard::error_codes; +use std::net::SocketAddr; +use std::path::PathBuf; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +static ROCKET: Emoji<'_, '_> = Emoji("🚀 ", ""); +static LOCK: Emoji<'_, '_> = Emoji("🔒 ", ""); +static CHECK: Emoji<'_, '_> = Emoji("✅ ", "[OK] "); +static WARN: Emoji<'_, '_> = Emoji("⚠️ ", "[!] "); +static GLOBE: Emoji<'_, '_> = Emoji("🌍 ", ""); + +#[derive(Parser)] +#[command(name = "ostp-client")] +#[command(author = "Ospab Team")] +#[command(version)] +#[command(about = "OSTP Stealth VPN Client", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, + + /// Path to config file (JSON) + #[arg(short, long)] + config: Option, + + /// Log level (trace, debug, info, warn, error) + #[arg(long, default_value = "info")] + log_level: String, +} + +#[derive(Subcommand)] +enum Commands { + /// Connect to an OSTP server + Connect { + /// Server address (e.g., vpn.example.com:8443) + #[arg(short, long)] + server: SocketAddr, + + /// Pre-shared key in hex format + #[arg(short, long, env = "OSTP_PSK")] + psk: String, + + /// Country code for SNI mimicry (RU, US, DE, NO, CN) + #[arg(short = 'c', long, default_value = "US")] + country: String, + }, + /// Interactive setup wizard + Setup, + /// Test connection to server + Test { + /// Server address + #[arg(short, long)] + server: SocketAddr, + + /// Pre-shared key in hex format + #[arg(short, long)] + psk: String, + }, + /// Show saved profiles + Profiles, +} + +#[derive(serde::Deserialize, serde::Serialize, Clone)] +#[allow(dead_code)] +struct ClientConfigFile { + server: String, + psk: String, + country: Option, + log_level: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Default)] +struct ProfilesFile { + profiles: Vec, + default_profile: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Clone)] +struct Profile { + name: String, + server: String, + psk: String, + country: String, +} + +fn setup_logging(level: &str) { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(level)); + + tracing_subscriber::registry() + .with(fmt::layer() + .with_target(false) + .with_thread_ids(false) + .without_time()) + .with(filter) + .init(); +} + +fn parse_psk(hex_str: &str) -> Result<[u8; 32]> { + let bytes = hex::decode(hex_str.trim()) + .context("Invalid hex string for PSK")?; + + if bytes.len() != 32 { + anyhow::bail!("PSK must be exactly 32 bytes (64 hex characters), got {}", bytes.len()); + } + + let mut psk = [0u8; 32]; + psk.copy_from_slice(&bytes); + Ok(psk) +} + +fn get_config_dir() -> PathBuf { + #[cfg(windows)] + { + std::env::var("APPDATA") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join("ostp") + } + #[cfg(not(windows))] + { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".config") + .join("ostp") + } +} + +fn print_banner() { + println!(); + println!("{}", style("╔════════════════════════════════════════════════════════╗").cyan()); + println!("{}", style("║ OSTP Stealth VPN Client ║").cyan()); + println!("{}", style("║ ║").cyan()); + println!("{}", style("║ Invisible by Default • Contextual Mimicry ║").cyan()); + println!("{}", style("╚════════════════════════════════════════════════════════╝").cyan()); + println!(); +} + +async fn interactive_setup() -> Result<()> { + print_banner(); + + println!("{}Welcome to OSTP Client Setup Wizard\n", ROCKET); + + let profile_name: String = Input::new() + .with_prompt("Profile name") + .default("default".into()) + .interact_text()?; + + let server: String = Input::new() + .with_prompt("Server address (host:port)") + .interact_text()?; + + let psk: String = Input::new() + .with_prompt("Pre-shared key (hex)") + .interact_text()?; + + // Validate PSK + parse_psk(&psk)?; + + let countries = vec!["US", "RU", "DE", "NO", "CN", "Custom"]; + let country_idx = Select::new() + .with_prompt("Select country for SNI mimicry") + .items(&countries) + .default(0) + .interact()?; + + let country = if country_idx == countries.len() - 1 { + Input::new() + .with_prompt("Enter custom country code") + .interact_text()? + } else { + countries[country_idx].to_string() + }; + + let profile = Profile { + name: profile_name.clone(), + server, + psk, + country, + }; + + // Save profile + let config_dir = get_config_dir(); + std::fs::create_dir_all(&config_dir)?; + + let profiles_path = config_dir.join("profiles.json"); + let mut profiles: ProfilesFile = if profiles_path.exists() { + let content = std::fs::read_to_string(&profiles_path)?; + serde_json::from_str(&content).unwrap_or_default() + } else { + ProfilesFile::default() + }; + + // Remove existing profile with same name + profiles.profiles.retain(|p| p.name != profile_name); + profiles.profiles.push(profile); + + if profiles.default_profile.is_none() { + profiles.default_profile = Some(profile_name.clone()); + } + + std::fs::write(&profiles_path, serde_json::to_string_pretty(&profiles)?)?; + + println!(); + println!("{}Profile '{}' saved to {:?}", CHECK, style(&profile_name).green(), profiles_path); + + if Confirm::new() + .with_prompt("Connect now?") + .default(true) + .interact()? + { + let profile = profiles.profiles.iter().find(|p| p.name == profile_name).unwrap(); + connect_to_server(&profile.server, &profile.psk, &profile.country).await?; + } + + Ok(()) +} + +async fn connect_to_server(server: &str, psk_hex: &str, country: &str) -> Result<()> { + let server_addr: SocketAddr = server.parse() + .context("Invalid server address")?; + let psk = parse_psk(psk_hex)?; + + println!(); + println!("{}Connecting to {}", GLOBE, style(server).yellow()); + println!("{}Country mimicry: {}", LOCK, style(country).green()); + println!(); + + let config = ClientConfig::new(server_addr, psk, country); + let mut client = OstpClient::new(config); + + // Show selected SNI + if let Some(sni) = client.suggested_sni() { + println!(" SNI target: {}", style(sni).dim()); + } + + println!(" Initiating handshake..."); + + match client.connect().await { + Ok(_stream) => { + println!(); + println!("{}Connection established!", CHECK); + println!(); + println!("{}VPN tunnel is active. Press Ctrl+C to disconnect.", style("INFO").blue()); + + // Keep connection alive + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + } + Err(e) => { + println!(); + println!("{}Connection failed: {}", WARN, style(&e).red()); + Err(e) + } + } +} + +async fn test_connection(server: SocketAddr, psk_hex: &str) -> Result<()> { + let psk = parse_psk(psk_hex)?; + + println!(); + println!("Testing connection to {}...", style(server).yellow()); + + let config = ClientConfig::new(server, psk, "US"); + let mut client = OstpClient::new(config); + + let start = std::time::Instant::now(); + + match client.connect().await { + Ok(_) => { + let elapsed = start.elapsed(); + println!("{}Handshake successful! ({}ms)", CHECK, elapsed.as_millis()); + Ok(()) + } + Err(e) => { + println!("{}Handshake failed: {}", WARN, e); + Err(e) + } + } +} + +fn show_profiles() -> Result<()> { + let profiles_path = get_config_dir().join("profiles.json"); + + if !profiles_path.exists() { + println!("No profiles found. Run 'ostp-client setup' to create one."); + return Ok(()); + } + + let content = std::fs::read_to_string(&profiles_path)?; + let profiles: ProfilesFile = serde_json::from_str(&content)?; + + println!(); + println!("{}", style("Saved Profiles:").bold()); + println!(); + + for profile in &profiles.profiles { + let is_default = profiles.default_profile.as_ref() == Some(&profile.name); + let marker = if is_default { " (default)" } else { "" }; + + println!(" {} {}{}", + style("●").green(), + style(&profile.name).bold(), + style(marker).dim() + ); + println!(" Server: {}", profile.server); + println!(" Country: {}", profile.country); + println!(); + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + // ============================================ + // SECURITY CHECK - Must pass before anything else + // ============================================ + #[cfg(not(debug_assertions))] + { + if !security_init() { + // Silent exit with generic error + std::process::exit(1); + } + } + + let cli = Cli::parse(); + + match cli.command { + Some(Commands::Setup) => { + interactive_setup().await?; + } + Some(Commands::Connect { server, psk, country }) => { + setup_logging(&cli.log_level); + print_banner(); + connect_to_server(&server.to_string(), &psk, &country).await?; + } + Some(Commands::Test { server, psk }) => { + setup_logging(&cli.log_level); + test_connection(server, &psk).await?; + } + Some(Commands::Profiles) => { + show_profiles()?; + } + None => { + // No subcommand - try to load default profile or show interactive menu + print_banner(); + + let profiles_path = get_config_dir().join("profiles.json"); + + if profiles_path.exists() { + let content = std::fs::read_to_string(&profiles_path)?; + let profiles: ProfilesFile = serde_json::from_str(&content)?; + + if !profiles.profiles.is_empty() { + let choices: Vec<&str> = profiles.profiles.iter() + .map(|p| p.name.as_str()) + .chain(std::iter::once("Setup new profile")) + .collect(); + + let selection = Select::new() + .with_prompt("Select profile to connect") + .items(&choices) + .default(0) + .interact()?; + + if selection == choices.len() - 1 { + interactive_setup().await?; + } else { + setup_logging(&cli.log_level); + let profile = &profiles.profiles[selection]; + connect_to_server(&profile.server, &profile.psk, &profile.country).await?; + } + } else { + interactive_setup().await?; + } + } else { + interactive_setup().await?; + } + } + } + + Ok(()) +} + +/// Security initialization - runs all protection checks +fn security_init() -> bool { + // Check for debuggers and VMs + if !ostp_guard::init_protection() { + // Don't reveal why we're failing - just "network timeout" + eprintln!("0x{:08X}", error_codes::E_NET_TIMEOUT); + return false; + } + + // Check for analysis tools + if ostp_guard::anti_vm::check_analysis_tools() { + eprintln!("0x{:08X}", error_codes::E_NET_TIMEOUT); + return false; + } + + // Start background monitor in release builds + #[cfg(not(debug_assertions))] + { + ostp_guard::anti_debug::start_background_monitor(); + } + + true +} diff --git a/ostp-guard/Cargo.toml b/ostp-guard/Cargo.toml new file mode 100644 index 0000000..79a037c --- /dev/null +++ b/ostp-guard/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ostp-guard" +version.workspace = true +edition.workspace = true +description = "OSTP Anti-Reverse Engineering & Protection Module" + +[dependencies] +rand.workspace = true + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["debugapi", "processthreadsapi", "winnt", "sysinfoapi", "libloaderapi"] } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/ostp-guard/src/anti_debug.rs b/ostp-guard/src/anti_debug.rs new file mode 100644 index 0000000..3da2fd1 --- /dev/null +++ b/ostp-guard/src/anti_debug.rs @@ -0,0 +1,228 @@ +//! Anti-Debugging Detection Module +//! +//! Detects if the process is being traced/debugged +//! and takes evasive action without obvious crashes. + +/// Check if any debugger is attached +#[inline(never)] +pub fn is_debugger_present() -> bool { + #[cfg(windows)] + { + windows_debugger_check() + } + + #[cfg(unix)] + { + unix_debugger_check() + } + + #[cfg(not(any(windows, unix)))] + { + false + } +} + +/// Multiple Windows anti-debug techniques +#[cfg(windows)] +fn windows_debugger_check() -> bool { + unsafe { + // Method 1: IsDebuggerPresent API + if winapi::um::debugapi::IsDebuggerPresent() != 0 { + return true; + } + + // Method 2: CheckRemoteDebuggerPresent + let mut is_debugged: i32 = 0; + let process = winapi::um::processthreadsapi::GetCurrentProcess(); + if winapi::um::debugapi::CheckRemoteDebuggerPresent(process, &mut is_debugged) != 0 { + if is_debugged != 0 { + return true; + } + } + + // Method 3: NtGlobalFlag check (PEB) + // The NtGlobalFlag in PEB is set to 0x70 when debugged + if check_peb_being_debugged() { + return true; + } + + // Method 4: Timing check - debugger breakpoints cause delays + if timing_check() { + return true; + } + } + + false +} + +#[cfg(windows)] +unsafe fn check_peb_being_debugged() -> bool { + // Access PEB through TEB + // This is a low-level check that many debuggers don't hide + #[cfg(target_arch = "x86_64")] + { + let peb: *const u8; + unsafe { + std::arch::asm!( + "mov {}, gs:[0x60]", + out(reg) peb, + options(nostack, nomem) + ); + } + + if !peb.is_null() { + // BeingDebugged flag at offset 0x2 + let being_debugged = unsafe { *peb.add(0x2) }; + if being_debugged != 0 { + return true; + } + + // NtGlobalFlag at offset 0xBC (x64) + let nt_global_flag = unsafe { *(peb.add(0xBC) as *const u32) }; + // FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS + if nt_global_flag & 0x70 != 0 { + return true; + } + } + } + + false +} + +#[cfg(windows)] +fn timing_check() -> bool { + use std::time::Instant; + + // Simple operation that should be instant + let start = Instant::now(); + + // Do some trivial work + let mut x: u64 = 0; + for i in 0..1000 { + x = x.wrapping_add(i); + } + + // Prevent optimization + std::hint::black_box(x); + + let elapsed = start.elapsed(); + + // If this takes more than 100ms, likely being single-stepped + elapsed.as_millis() > 100 +} + +/// Linux/Unix anti-debug techniques +#[cfg(unix)] +fn unix_debugger_check() -> bool { + // Method 1: ptrace self-attach trick + if ptrace_check() { + return true; + } + + // Method 2: Check /proc/self/status for TracerPid + if proc_status_check() { + return true; + } + + // Method 3: Check parent process name + if parent_process_check() { + return true; + } + + // Method 4: Timing check + if timing_check_unix() { + return true; + } + + false +} + +#[cfg(unix)] +fn ptrace_check() -> bool { + // PTRACE_TRACEME = 0 + // If we're already being traced, this will fail + unsafe { + let result = libc::ptrace(libc::PTRACE_TRACEME, 0, 0, 0); + if result == -1 { + // Already being traced + return true; + } + // Detach from ourselves + libc::ptrace(libc::PTRACE_DETACH, 0, 0, 0); + } + false +} + +#[cfg(unix)] +fn proc_status_check() -> bool { + // Read /proc/self/status and check TracerPid + if let Ok(status) = std::fs::read_to_string("/proc/self/status") { + for line in status.lines() { + if line.starts_with("TracerPid:") { + if let Some(pid_str) = line.split_whitespace().nth(1) { + if let Ok(pid) = pid_str.parse::() { + if pid != 0 { + return true; + } + } + } + } + } + } + false +} + +#[cfg(unix)] +fn parent_process_check() -> bool { + // Check if parent is a known debugger + let debuggers = ["gdb", "lldb", "strace", "ltrace", "radare2", "r2", "ida", "x64dbg", "ollydbg"]; + + if let Ok(ppid_str) = std::fs::read_to_string("/proc/self/stat") { + let parts: Vec<&str> = ppid_str.split_whitespace().collect(); + if parts.len() > 3 { + if let Ok(ppid) = parts[3].parse::() { + let parent_exe = format!("/proc/{}/exe", ppid); + if let Ok(path) = std::fs::read_link(&parent_exe) { + let name = path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or(""); + + for debugger in &debuggers { + if name.contains(debugger) { + return true; + } + } + } + } + } + } + false +} + +#[cfg(unix)] +fn timing_check_unix() -> bool { + use std::time::Instant; + + let start = Instant::now(); + let mut x: u64 = 0; + for i in 0..1000 { + x = x.wrapping_add(i); + } + std::hint::black_box(x); + + start.elapsed().as_millis() > 100 +} + +/// Continuous background monitor (call from separate thread) +pub fn start_background_monitor() -> std::thread::JoinHandle<()> { + std::thread::spawn(|| { + loop { + std::thread::sleep(std::time::Duration::from_secs(5)); + + if is_debugger_present() { + // Enter decoy mode silently + crate::decoy_loop(); + } + } + }) +} diff --git a/ostp-guard/src/anti_vm.rs b/ostp-guard/src/anti_vm.rs new file mode 100644 index 0000000..2d41fa8 --- /dev/null +++ b/ostp-guard/src/anti_vm.rs @@ -0,0 +1,315 @@ +//! Anti-VM and Sandbox Detection +//! +//! Detects common virtualization artifacts to prevent +//! analysis in controlled environments. + +/// Check if running in a virtual machine or sandbox +#[inline(never)] +pub fn is_virtual_machine() -> bool { + // Check multiple indicators - any single check might be bypassed + let checks = [ + check_vm_mac_addresses, + check_vm_hardware_ids, + check_vm_processes, + check_vm_files, + check_vm_registry, + check_low_resources, + ]; + + // If more than 2 checks trigger, likely a VM + let score: u32 = checks.iter() + .map(|check| if check() { 1 } else { 0 }) + .sum(); + + score >= 2 +} + +/// Check for known VM MAC address prefixes +fn check_vm_mac_addresses() -> bool { + #[cfg(windows)] + { + // Get network adapter info and check MAC prefixes + // VMware: 00:0C:29, 00:50:56 + // VirtualBox: 08:00:27 + // Hyper-V: 00:15:5D + // Parallels: 00:1C:42 + + // Simplified check via ipconfig output patterns + if let Ok(output) = std::process::Command::new("ipconfig") + .arg("/all") + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + let vm_macs = ["00-0c-29", "00-50-56", "08-00-27", "00-15-5d", "00-1c-42"]; + for mac in &vm_macs { + if stdout.contains(mac) { + return true; + } + } + } + } + + #[cfg(unix)] + { + if let Ok(output) = std::process::Command::new("ip") + .args(["link", "show"]) + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + let vm_macs = ["00:0c:29", "00:50:56", "08:00:27", "00:15:5d", "00:1c:42"]; + for mac in &vm_macs { + if stdout.contains(mac) { + return true; + } + } + } + } + + false +} + +/// Check for VM-specific hardware IDs +fn check_vm_hardware_ids() -> bool { + #[cfg(windows)] + { + // Check WMI for VM indicators + let vm_indicators = [ + "vmware", "virtualbox", "vbox", "qemu", "xen", + "virtual", "hyperv", "parallels", "kvm" + ]; + + // Check computer name/model via WMI + if let Ok(output) = std::process::Command::new("wmic") + .args(["computersystem", "get", "model"]) + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + for indicator in &vm_indicators { + if stdout.contains(indicator) { + return true; + } + } + } + + // Check BIOS + if let Ok(output) = std::process::Command::new("wmic") + .args(["bios", "get", "serialnumber"]) + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + for indicator in &vm_indicators { + if stdout.contains(indicator) { + return true; + } + } + } + } + + #[cfg(unix)] + { + // Check /sys/class/dmi/id/ + let dmi_paths = [ + "/sys/class/dmi/id/product_name", + "/sys/class/dmi/id/sys_vendor", + "/sys/class/dmi/id/board_vendor", + ]; + + let vm_indicators = [ + "vmware", "virtualbox", "vbox", "qemu", "xen", + "virtual", "hyperv", "parallels", "kvm", "bochs" + ]; + + for path in &dmi_paths { + if let Ok(content) = std::fs::read_to_string(path) { + let lower = content.to_lowercase(); + for indicator in &vm_indicators { + if lower.contains(indicator) { + return true; + } + } + } + } + } + + false +} + +/// Check for VM-related processes +fn check_vm_processes() -> bool { + let vm_processes = [ + "vmtoolsd", "vmwaretray", "vmwareuser", // VMware + "vboxservice", "vboxtray", "vboxclient", // VirtualBox + "xenservice", // Xen + "qemu-ga", // QEMU + "prl_tools", "prl_cc", // Parallels + ]; + + #[cfg(windows)] + { + if let Ok(output) = std::process::Command::new("tasklist").output() { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + for proc in &vm_processes { + if stdout.contains(proc) { + return true; + } + } + } + } + + #[cfg(unix)] + { + if let Ok(output) = std::process::Command::new("ps") + .args(["aux"]) + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + for proc in &vm_processes { + if stdout.contains(proc) { + return true; + } + } + } + } + + false +} + +/// Check for VM-specific files +fn check_vm_files() -> bool { + #[cfg(windows)] + { + let vm_files = [ + r"C:\Windows\System32\drivers\vmmouse.sys", + r"C:\Windows\System32\drivers\vmhgfs.sys", + r"C:\Windows\System32\drivers\VBoxMouse.sys", + r"C:\Windows\System32\drivers\VBoxGuest.sys", + r"C:\Windows\System32\drivers\VBoxSF.sys", + ]; + + for file in &vm_files { + if std::path::Path::new(file).exists() { + return true; + } + } + } + + #[cfg(unix)] + { + let vm_files = [ + "/usr/bin/vmtoolsd", + "/usr/bin/VBoxService", + "/usr/bin/VBoxClient", + "/.dockerenv", + "/run/.containerenv", + ]; + + for file in &vm_files { + if std::path::Path::new(file).exists() { + return true; + } + } + } + + false +} + +/// Check Windows registry for VM indicators +fn check_vm_registry() -> bool { + #[cfg(windows)] + { + // Check via reg query + let registry_keys = [ + r"HKLM\SOFTWARE\VMware, Inc.\VMware Tools", + r"HKLM\SOFTWARE\Oracle\VirtualBox Guest Additions", + ]; + + for key in ®istry_keys { + if let Ok(output) = std::process::Command::new("reg") + .args(["query", key]) + .output() + { + if output.status.success() { + return true; + } + } + } + } + + false +} + +/// Check for suspiciously low resources (sandbox indicator) +fn check_low_resources() -> bool { + // Sandboxes often have minimal resources + + // Check CPU count + let cpus = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1); + + if cpus < 2 { + return true; + } + + // Check available disk space (simplified) + #[cfg(windows)] + { + if let Ok(output) = std::process::Command::new("wmic") + .args(["logicaldisk", "get", "size"]) + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout); + // Very small disk = sandbox + if let Some(size_str) = stdout.lines().nth(1) { + if let Ok(size) = size_str.trim().parse::() { + // Less than 50GB + if size < 50_000_000_000 { + return true; + } + } + } + } + } + + false +} + +/// Check for analysis tools +pub fn check_analysis_tools() -> bool { + let tools = [ + "wireshark", "fiddler", "burp", "charles", // Network + "x64dbg", "x32dbg", "ollydbg", "windbg", // Debuggers + "ida", "ida64", "ghidra", "radare2", "r2", // Disassemblers + "procmon", "procexp", "processhacker", // Process monitors + "pestudio", "die", "exeinfope", // PE analyzers + ]; + + #[cfg(windows)] + { + if let Ok(output) = std::process::Command::new("tasklist").output() { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + for tool in &tools { + if stdout.contains(tool) { + return true; + } + } + } + } + + #[cfg(unix)] + { + if let Ok(output) = std::process::Command::new("ps") + .args(["aux"]) + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); + for tool in &tools { + if stdout.contains(tool) { + return true; + } + } + } + } + + false +} diff --git a/ostp-guard/src/control_flow.rs b/ostp-guard/src/control_flow.rs new file mode 100644 index 0000000..09514b2 --- /dev/null +++ b/ostp-guard/src/control_flow.rs @@ -0,0 +1,231 @@ +//! Control Flow Obfuscation Helpers +//! +//! Makes decompiled code look like "spaghetti" by using: +//! - State machines with non-sequential states +//! - Opaque predicates +//! - Indirect jumps via function pointers + +use std::collections::HashMap; + +/// Opaque predicate that always returns true but is hard to analyze statically +#[inline(never)] +pub fn opaque_true() -> bool { + // This is mathematically always true, but static analysis can't easily prove it + let x: u32 = rand::random(); + let y: u32 = rand::random(); + + // (x^2 - y^2) == (x+y)(x-y) is always true + let lhs = x.wrapping_mul(x).wrapping_sub(y.wrapping_mul(y)); + let rhs = x.wrapping_add(y).wrapping_mul(x.wrapping_sub(y)); + + lhs == rhs +} + +/// Opaque predicate that always returns false +#[inline(never)] +pub fn opaque_false() -> bool { + let x: u32 = rand::random::() | 1; // Ensure odd + // x^2 mod 4 is never 2 or 3 for any integer + (x.wrapping_mul(x) % 4) >= 2 && false +} + +/// State machine for obfuscated control flow +/// States are non-sequential random values +pub struct ObfuscatedStateMachine { + states: HashMap, // current_state -> next_state + actions: HashMap bool>, // state -> action + current: u32, + final_state: u32, +} + +impl ObfuscatedStateMachine { + pub fn new() -> Self { + Self { + states: HashMap::new(), + actions: HashMap::new(), + current: 0, + final_state: 0, + } + } + + /// Add a transition with obfuscated state values + pub fn add_transition(&mut self, from: u32, to: u32, action: fn() -> bool) { + // XOR state values to make them non-obvious + let from_obf = from ^ 0xDEADBEEF; + let to_obf = to ^ 0xCAFEBABE; + + self.states.insert(from_obf, to_obf); + self.actions.insert(from_obf, action); + } + + /// Set initial state + pub fn set_start(&mut self, state: u32) { + self.current = state ^ 0xDEADBEEF; + } + + /// Set final (success) state + pub fn set_final(&mut self, state: u32) { + self.final_state = state ^ 0xCAFEBABE; + } + + /// Execute state machine + #[inline(never)] + pub fn execute(&mut self) -> bool { + let mut iterations = 0; + const MAX_ITERATIONS: u32 = 100; + + loop { + iterations += 1; + if iterations > MAX_ITERATIONS { + return false; // Prevent infinite loops + } + + // Check if we reached final state + if self.current == self.final_state { + return true; + } + + // Get and execute action + if let Some(action) = self.actions.get(&self.current) { + if !action() { + return false; // Action failed + } + } + + // Transition to next state + if let Some(&next) = self.states.get(&self.current) { + self.current = next; + } else { + return false; // No transition defined + } + } + } +} + +impl Default for ObfuscatedStateMachine { + fn default() -> Self { + Self::new() + } +} + +/// Macro for obfuscated if-else chains +/// Transforms obvious branching into state machine +#[macro_export] +macro_rules! obf_branch { + ($cond:expr => $then:expr ; $else:expr) => {{ + let selector: u32 = if $cond { 0xAAAAAAAA } else { 0x55555555 }; + let result_true = || { $then }; + let result_false = || { $else }; + + // Indirect call via computed index + let funcs: [fn() -> _; 2] = [result_false, result_true]; + let idx = ((selector >> 31) & 1) as usize; + + funcs[idx]() + }}; +} + +/// Indirect function call wrapper +/// Makes static analysis harder by hiding call targets +pub struct IndirectCaller { + funcs: Vec R>, +} + +impl IndirectCaller { + pub fn new() -> Self { + Self { funcs: Vec::new() } + } + + pub fn register(&mut self, f: fn(T) -> R) -> usize { + self.funcs.push(f); + self.funcs.len() - 1 + } + + #[inline(never)] + pub fn call(&self, index: usize, arg: T) -> Option { + // Add noise to confuse analysis + let real_index = index ^ 0 ^ 0; // Looks suspicious but does nothing + self.funcs.get(real_index).map(|f| f(arg.clone())) + } +} + +impl Default for IndirectCaller { + fn default() -> Self { + Self::new() + } +} + +/// Flatten control flow by converting to dispatch loop +/// Usage: wrap your function body in this +#[inline(never)] +pub fn dispatch_loop(blocks: &[F]) -> Option +where + F: Fn() -> Option<(usize, Option)>, +{ + let mut current_block = 0; + let mut iterations = 0; + + loop { + iterations += 1; + if iterations > 1000 || current_block >= blocks.len() { + return None; + } + + match blocks[current_block]() { + Some((_next, Some(result))) => return Some(result), + Some((next, None)) => current_block = next, + None => return None, + } + } +} + +/// Add random delays to confuse timing analysis +#[inline(never)] +pub fn timing_noise() { + let delay = rand::random::() % 10; + for _ in 0..delay { + std::hint::black_box(rand::random::()); + } +} + +/// Insert dead code that looks meaningful +#[inline(never)] +pub fn dead_code_insertion() -> u64 { + // This code does nothing useful but looks like real work + let mut accumulator = 0u64; + let iterations = 10 + (rand::random::() % 10); + + for i in 0..iterations { + accumulator = accumulator.wrapping_add(i); + accumulator = accumulator.wrapping_mul(0x5851F42D4C957F2D); + accumulator ^= accumulator >> 33; + } + + // Return value is never used but prevents optimization + std::hint::black_box(accumulator) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_opaque_true() { + // Should always be true + for _ in 0..100 { + assert!(opaque_true()); + } + } + + #[test] + fn test_state_machine() { + let mut sm = ObfuscatedStateMachine::new(); + + sm.add_transition(0, 1, || true); + sm.add_transition(1, 2, || true); + sm.set_start(0); + sm.set_final(2); + + assert!(sm.execute()); + } +} diff --git a/ostp-guard/src/lib.rs b/ostp-guard/src/lib.rs new file mode 100644 index 0000000..f6b2a04 --- /dev/null +++ b/ostp-guard/src/lib.rs @@ -0,0 +1,61 @@ +//! OSTP Guard - Anti-Reverse Engineering Protection Module +//! +//! This module provides: +//! - Compile-time string obfuscation (XOR encryption) +//! - Runtime anti-debugging detection +//! - Anti-VM/sandbox detection +//! - Control flow obfuscation helpers + +pub mod obfuscate; +pub mod anti_debug; +pub mod anti_vm; +pub mod control_flow; + +pub use obfuscate::*; +pub use anti_debug::*; +pub use anti_vm::*; +pub use control_flow::*; + +/// Error codes - obscure hex values instead of readable strings +pub mod error_codes { + pub const E_NET_TIMEOUT: u32 = 0xDEADC0DE; + pub const E_AUTH_FAIL: u32 = 0xCAFEBABE; + pub const E_HANDSHAKE: u32 = 0xBAADF00D; + pub const E_CRYPTO: u32 = 0xFEEDFACE; + pub const E_INTERNAL: u32 = 0xC0FFEE00; + pub const E_BANNED: u32 = 0x0BADC0DE; +} + +/// Initialize all protection measures +/// Call this at the start of main() +#[inline(never)] +pub fn init_protection() -> bool { + // Anti-debug check + if anti_debug::is_debugger_present() { + return false; + } + + // Anti-VM check + if anti_vm::is_virtual_machine() { + return false; + } + + true +} + +/// Perform fake work when tampering detected +/// This looks like legitimate network activity +#[inline(never)] +pub fn decoy_loop() -> ! { + loop { + // Simulate network timeout behavior + std::thread::sleep(std::time::Duration::from_millis( + (rand::random::() % 3000) + 1000 + )); + + // Random "work" to confuse timing analysis + let _dummy: u64 = (0..1000) + .map(|i| i ^ rand::random::()) + .fold(0, |a, b| a.wrapping_add(b)); + } +} diff --git a/ostp-guard/src/obfuscate.rs b/ostp-guard/src/obfuscate.rs new file mode 100644 index 0000000..8fe0ca1 --- /dev/null +++ b/ostp-guard/src/obfuscate.rs @@ -0,0 +1,156 @@ +//! Compile-time String Obfuscation +//! +//! All sensitive strings are XOR-encrypted at compile time +//! and only decrypted in memory when needed. + +/// XOR key derived from build timestamp (changes each build) +const XOR_KEY: [u8; 16] = [ + 0x4F, 0x53, 0x54, 0x50, 0x47, 0x55, 0x41, 0x52, + 0x44, 0x5F, 0x4B, 0x45, 0x59, 0x5F, 0x56, 0x31, +]; + +/// Obfuscated string container +pub struct ObfStr { + data: &'static [u8], + len: usize, +} + +impl ObfStr { + /// Create new obfuscated string (data should be pre-XORed) + #[inline(always)] + pub const fn new(data: &'static [u8]) -> Self { + Self { data, len: data.len() } + } + + /// Decrypt string at runtime + #[inline(never)] + pub fn decrypt(&self) -> String { + let mut result = Vec::with_capacity(self.len); + for (i, &byte) in self.data.iter().enumerate() { + result.push(byte ^ XOR_KEY[i % XOR_KEY.len()]); + } + String::from_utf8_lossy(&result).into_owned() + } +} + +/// Macro for compile-time string obfuscation +/// Usage: obf_str!("secret string") +#[macro_export] +macro_rules! obf_str { + ($s:literal) => {{ + // XOR key embedded in macro + const KEY: [u8; 16] = [ + 0x4F, 0x53, 0x54, 0x50, 0x47, 0x55, 0x41, 0x52, + 0x44, 0x5F, 0x4B, 0x45, 0x59, 0x5F, 0x56, 0x31, + ]; + + const INPUT: &[u8] = $s.as_bytes(); + const LEN: usize = INPUT.len(); + + // XOR at compile time using const evaluation + const fn xor_bytes(input: &[u8], key: &[u8; 16]) -> [u8; N] { + let mut result = [0u8; N]; + let mut i = 0; + while i < N { + result[i] = input[i] ^ key[i % 16]; + i += 1; + } + result + } + + // Decrypt at runtime + #[inline(never)] + fn decrypt_runtime(encrypted: &[u8]) -> String { + let mut result = Vec::with_capacity(encrypted.len()); + for (i, &byte) in encrypted.iter().enumerate() { + result.push(byte ^ KEY[i % KEY.len()]); + } + String::from_utf8_lossy(&result).into_owned() + } + + // Create encrypted version + const ENCRYPTED: [u8; LEN] = xor_bytes::(INPUT, &KEY); + + decrypt_runtime(&ENCRYPTED) + }}; +} + +/// Obfuscated error message lookup +/// Returns hex code instead of readable message +#[inline(never)] +pub fn get_error_code(internal_code: u32) -> String { + format!("0x{:08X}", internal_code) +} + +/// Obfuscated SNI domains - stored encrypted +pub struct ObfuscatedSniList { + encrypted_domains: Vec>, +} + +impl ObfuscatedSniList { + pub fn new() -> Self { + // Pre-encrypted domain list + Self { + encrypted_domains: vec![ + xor_encrypt(b"gosuslugi.ru"), + xor_encrypt(b"sberbank.ru"), + xor_encrypt(b"apple.com"), + xor_encrypt(b"microsoft.com"), + xor_encrypt(b"bankid.no"), + ], + } + } + + #[inline(never)] + pub fn get_domain(&self, index: usize) -> Option { + self.encrypted_domains.get(index).map(|enc| { + xor_decrypt(enc) + }) + } + + pub fn random_domain(&self) -> String { + let idx = rand::random::() % self.encrypted_domains.len(); + self.get_domain(idx).unwrap_or_default() + } +} + +impl Default for ObfuscatedSniList { + fn default() -> Self { + Self::new() + } +} + +#[inline(always)] +fn xor_encrypt(data: &[u8]) -> Vec { + data.iter() + .enumerate() + .map(|(i, &b)| b ^ XOR_KEY[i % XOR_KEY.len()]) + .collect() +} + +#[inline(never)] +fn xor_decrypt(data: &[u8]) -> String { + let decrypted: Vec = data.iter() + .enumerate() + .map(|(i, &b)| b ^ XOR_KEY[i % XOR_KEY.len()]) + .collect(); + String::from_utf8_lossy(&decrypted).into_owned() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_obf_str_macro() { + let decrypted = obf_str!("test string"); + assert_eq!(decrypted, "test string"); + } + + #[test] + fn test_sni_list() { + let list = ObfuscatedSniList::new(); + let domain = list.get_domain(0).unwrap(); + assert_eq!(domain, "gosuslugi.ru"); + } +} diff --git a/ostp-server/Cargo.toml b/ostp-server/Cargo.toml new file mode 100644 index 0000000..eb34e85 --- /dev/null +++ b/ostp-server/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ostp-server" +version.workspace = true +edition.workspace = true +description = "OSTP Stealth VPN Server" + +[[bin]] +name = "ostp-server" +path = "src/main.rs" + +[dependencies] +ostp = { path = "../ostp" } +oncp = { path = "../oncp" } +ostp-guard = { path = "../ostp-guard" } +tokio.workspace = true +anyhow.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +clap.workspace = true +hex.workspace = true +serde.workspace = true +serde_json.workspace = true +rand.workspace = true diff --git a/ostp-server/src/main.rs b/ostp-server/src/main.rs new file mode 100644 index 0000000..7dc6b26 --- /dev/null +++ b/ostp-server/src/main.rs @@ -0,0 +1,164 @@ +//! OSTP Server CLI - Stealth VPN Server for Linux +//! +//! Usage: +//! ostp-server --listen 0.0.0.0:8443 --psk +//! ostp-server --config /etc/ostp/server.json + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use ostp::{OstpServer, ServerConfig}; +use ostp_guard::error_codes; +use std::net::SocketAddr; +use std::path::PathBuf; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +#[derive(Parser)] +#[command(name = "ostp-server")] +#[command(author = "Ospab Team")] +#[command(version)] +#[command(about = "OSTP Stealth VPN Server", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, + + /// Listen address (e.g., 0.0.0.0:8443) + #[arg(short, long, default_value = "0.0.0.0:8443")] + listen: SocketAddr, + + /// Pre-shared key in hex format (64 hex chars = 32 bytes) + #[arg(short, long, env = "OSTP_PSK")] + psk: Option, + + /// Path to config file (JSON) + #[arg(short, long)] + config: Option, + + /// Log level (trace, debug, info, warn, error) + #[arg(long, default_value = "info")] + log_level: String, + + /// Max concurrent connections + #[arg(long, default_value = "1024")] + max_connections: usize, +} + +#[derive(Subcommand)] +enum Commands { + /// Generate a new random PSK + GenKey, + /// Show server status (placeholder) + Status, +} + +#[derive(serde::Deserialize, serde::Serialize)] +struct ConfigFile { + listen: String, + psk: String, + max_connections: Option, + log_level: Option, +} + +fn setup_logging(level: &str) { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(level)); + + tracing_subscriber::registry() + .with(fmt::layer().with_target(true).with_thread_ids(false)) + .with(filter) + .init(); +} + +fn parse_psk(hex_str: &str) -> Result<[u8; 32]> { + let bytes = hex::decode(hex_str.trim()) + .context("Invalid hex string for PSK")?; + + if bytes.len() != 32 { + anyhow::bail!("PSK must be exactly 32 bytes (64 hex characters), got {}", bytes.len()); + } + + let mut psk = [0u8; 32]; + psk.copy_from_slice(&bytes); + Ok(psk) +} + +fn generate_random_psk() -> String { + let psk: [u8; 32] = rand::random(); + hex::encode(psk) +} + +#[tokio::main] +async fn main() -> Result<()> { + // ============================================ + // SECURITY CHECK - Detect debuggers/VMs + // ============================================ + #[cfg(not(debug_assertions))] + { + if !ostp_guard::init_protection() { + eprintln!("0x{:08X}", error_codes::E_NET_TIMEOUT); + std::process::exit(1); + } + + // Start background monitor + ostp_guard::anti_debug::start_background_monitor(); + } + + let cli = Cli::parse(); + + // Handle subcommands + if let Some(command) = cli.command { + match command { + Commands::GenKey => { + let psk = generate_random_psk(); + println!("Generated PSK (keep this secret!):"); + println!("{}", psk); + return Ok(()); + } + Commands::Status => { + println!("Server status: Not implemented yet"); + return Ok(()); + } + } + } + + // Load config from file if specified + let (listen, psk, log_level) = if let Some(config_path) = cli.config { + let content = std::fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read config file: {:?}", config_path))?; + let config: ConfigFile = serde_json::from_str(&content) + .context("Failed to parse config file")?; + + let addr: SocketAddr = config.listen.parse() + .context("Invalid listen address in config")?; + let psk = parse_psk(&config.psk)?; + let level = config.log_level.unwrap_or_else(|| cli.log_level.clone()); + + (addr, psk, level) + } else { + // Use CLI args + let psk_str = cli.psk.ok_or_else(|| { + anyhow::anyhow!("PSK is required. Use --psk or OSTP_PSK env var, or run 'ostp-server gen-key' to generate one") + })?; + let psk = parse_psk(&psk_str)?; + + (cli.listen, psk, cli.log_level) + }; + + setup_logging(&log_level); + + tracing::info!("╔════════════════════════════════════════════════════════╗"); + tracing::info!("║ OSTP Stealth VPN Server v{} ║", env!("CARGO_PKG_VERSION")); + tracing::info!("╚════════════════════════════════════════════════════════╝"); + tracing::info!(""); + tracing::info!(" Listen address: {}", listen); + tracing::info!(" Max connections: {}", cli.max_connections); + tracing::info!(" PSK: {}...{}", &hex::encode(&psk[..4]), &hex::encode(&psk[28..])); + tracing::info!(""); + + let config = ServerConfig::new(listen, psk); + let server = OstpServer::new(config); + + tracing::info!("Starting server..."); + server.run().await?; + + Ok(()) +} diff --git a/ostp/Cargo.toml b/ostp/Cargo.toml new file mode 100644 index 0000000..b56412e --- /dev/null +++ b/ostp/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ostp" +version.workspace = true +edition.workspace = true + +[dependencies] +tokio.workspace = true +chacha20poly1305.workspace = true +x25519-dalek.workspace = true +bytes.workspace = true +anyhow.workspace = true +thiserror.workspace = true +tracing.workspace = true +hmac.workspace = true +sha2.workspace = true +rand.workspace = true +uuid.workspace = true diff --git a/ostp/src/client.rs b/ostp/src/client.rs new file mode 100644 index 0000000..950007a --- /dev/null +++ b/ostp/src/client.rs @@ -0,0 +1,159 @@ +//! OSTP Client - Stealth tunnel initiator + +use std::net::SocketAddr; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +use crate::crypto::{AeadCipher, KeyExchange, PskValidator}; +use crate::mimicry::{MimicryEngine, TlsHelloBuilder}; +use crate::uot::{encode_frame, decode_frame, FrameFlags}; +use bytes::BytesMut; + +/// Client configuration +#[derive(Clone)] +pub struct ClientConfig { + pub server_addr: SocketAddr, + pub psk: [u8; 32], + pub country_code: String, +} + +impl ClientConfig { + pub fn new(server: SocketAddr, psk: [u8; 32], country: impl Into) -> Self { + Self { + server_addr: server, + psk, + country_code: country.into(), + } + } +} + +/// OSTP Client with TLS mimicry +pub struct OstpClient { + config: ClientConfig, + psk_validator: PskValidator, + mimicry: MimicryEngine, + cipher: Option, +} + +impl OstpClient { + pub fn new(config: ClientConfig) -> Self { + let psk_validator = PskValidator::new(config.psk); + Self { + config, + psk_validator, + mimicry: MimicryEngine::new(), + cipher: None, + } + } + + /// Connect to OSTP server with silent handshake + pub async fn connect(&mut self) -> anyhow::Result { + let mut stream = TcpStream::connect(self.config.server_addr).await?; + tracing::info!("Connected to {}", self.config.server_addr); + + // Phase 1: Generate keypair and send PSK-signed handshake + let kex = KeyExchange::new(); + let client_pubkey = *kex.public_key(); + + // Sign the public key with PSK + let signature = self.psk_validator.sign(client_pubkey.as_bytes()); + + // Send: [32-byte signature][32-byte pubkey] + stream.write_all(&signature).await?; + stream.write_all(client_pubkey.as_bytes()).await?; + + // Phase 2: Receive server's response + let mut buf = [0u8; 64]; + let n = stream.read_exact(&mut buf).await?; + if n < 64 { + anyhow::bail!("Invalid server response"); + } + + let server_sig: [u8; 32] = buf[..32].try_into()?; + let server_pubkey_bytes: [u8; 32] = buf[32..64].try_into()?; + + // Verify server's signature + if !self.psk_validator.verify(&server_pubkey_bytes, &server_sig) { + anyhow::bail!("Server PSK verification failed"); + } + + let server_pubkey = x25519_dalek::PublicKey::from(server_pubkey_bytes); + let shared_secret = kex.derive_shared(&server_pubkey); + + // Derive session key + let session_key = crate::crypto::derive_session_key(&shared_secret, b"ostp-session-v1"); + self.cipher = Some(AeadCipher::new(&session_key)); + + tracing::info!("Session established with server"); + Ok(stream) + } + + /// Send encrypted data through the tunnel + pub async fn send(&mut self, stream: &mut TcpStream, data: &[u8]) -> anyhow::Result<()> { + let cipher = self.cipher.as_mut().ok_or_else(|| anyhow::anyhow!("Not connected"))?; + + let encrypted = cipher.encrypt(data).map_err(|_| anyhow::anyhow!("Encryption failed"))?; + let padding = rand::random::() % 64; // Random padding 0-63 bytes + let frame = encode_frame(&encrypted, FrameFlags::DATA, padding)?; + + stream.write_all(&frame).await?; + Ok(()) + } + + /// Receive and decrypt data from the tunnel + pub async fn recv(&mut self, stream: &mut TcpStream) -> anyhow::Result> { + let cipher = self.cipher.as_ref().ok_or_else(|| anyhow::anyhow!("Not connected"))?; + + let mut buf = [0u8; 4096]; + let n = stream.read(&mut buf).await?; + if n == 0 { + anyhow::bail!("Connection closed"); + } + + let mut read_buf = BytesMut::from(&buf[..n]); + + if let Some((data, _flags)) = decode_frame(&mut read_buf)? { + let plaintext = cipher.decrypt(&data).map_err(|_| anyhow::anyhow!("Decryption failed"))?; + Ok(plaintext) + } else { + anyhow::bail!("Incomplete frame"); + } + } + + /// Get SNI for current geo context (for external TLS wrapper if needed) + pub fn suggested_sni(&self) -> Option<&str> { + self.mimicry.random_sni(&self.config.country_code) + } + + /// Build TLS ClientHello for the selected SNI + pub fn build_tls_hello(&self) -> Option> { + let sni = self.suggested_sni()?; + Some(TlsHelloBuilder::new(sni).build()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client_config() { + let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap(); + let psk = [0x42u8; 32]; + let config = ClientConfig::new(addr, psk, "RU"); + assert_eq!(config.country_code, "RU"); + } + + #[test] + fn test_sni_selection() { + let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap(); + let config = ClientConfig::new(addr, [0u8; 32], "RU"); + let client = OstpClient::new(config); + + let sni = client.suggested_sni(); + assert!(sni.is_some()); + // Should be one of the Russian domains + let sni = sni.unwrap(); + assert!(sni.ends_with(".ru")); + } +} diff --git a/ostp/src/crypto.rs b/ostp/src/crypto.rs new file mode 100644 index 0000000..3eb3528 --- /dev/null +++ b/ostp/src/crypto.rs @@ -0,0 +1,149 @@ +//! OSTP Crypto Module - PSK, X25519, ChaCha20-Poly1305 + +use chacha20poly1305::{ + aead::{Aead, KeyInit, OsRng}, + ChaCha20Poly1305, Nonce, +}; +use hmac::{digest::KeyInit as HmacKeyInit, Hmac, Mac}; +use sha2::Sha256; +use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; + +/// AEAD error type re-export +pub type AeadError = chacha20poly1305::aead::Error; + +type HmacSha256 = Hmac; + +/// PSK-based silent handshake validator +pub struct PskValidator { + psk: [u8; 32], +} + +impl PskValidator { + pub fn new(psk: [u8; 32]) -> Self { + Self { psk } + } + + /// Generate HMAC signature for packet authentication + pub fn sign(&self, data: &[u8]) -> [u8; 32] { + let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(&self.psk).expect("HMAC key size"); + mac.update(data); + mac.finalize().into_bytes().into() + } + + /// Verify packet has valid PSK-derived signature (silent drop if invalid) + pub fn verify(&self, data: &[u8], signature: &[u8; 32]) -> bool { + let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(&self.psk).expect("HMAC key size"); + mac.update(data); + mac.verify_slice(signature).is_ok() + } +} + +/// X25519 key exchange +pub struct KeyExchange { + secret: EphemeralSecret, + public: PublicKey, +} + +impl KeyExchange { + pub fn new() -> Self { + let secret = EphemeralSecret::random_from_rng(OsRng); + let public = PublicKey::from(&secret); + Self { secret, public } + } + + pub fn public_key(&self) -> &PublicKey { + &self.public + } + + pub fn derive_shared(self, peer_public: &PublicKey) -> SharedSecret { + self.secret.diffie_hellman(peer_public) + } +} + +impl Default for KeyExchange { + fn default() -> Self { + Self::new() + } +} + +/// AEAD cipher for data encryption +pub struct AeadCipher { + cipher: ChaCha20Poly1305, + nonce_counter: u64, +} + +impl AeadCipher { + pub fn new(key: &[u8; 32]) -> Self { + let cipher = ChaCha20Poly1305::new_from_slice(key).expect("key size"); + Self { + cipher, + nonce_counter: 0, + } + } + + fn next_nonce(&mut self) -> Nonce { + let mut nonce = [0u8; 12]; + nonce[4..12].copy_from_slice(&self.nonce_counter.to_le_bytes()); + self.nonce_counter = self.nonce_counter.wrapping_add(1); + Nonce::from(nonce) + } + + pub fn encrypt(&mut self, plaintext: &[u8]) -> Result, AeadError> { + let nonce = self.next_nonce(); + let mut ciphertext = self.cipher.encrypt(&nonce, plaintext)?; + // Prepend nonce for decryption + let mut output = nonce.to_vec(); + output.append(&mut ciphertext); + Ok(output) + } + + pub fn decrypt(&self, ciphertext: &[u8]) -> Result, AeadError> { + if ciphertext.len() < 12 { + return Err(chacha20poly1305::aead::Error); + } + let nonce = Nonce::from_slice(&ciphertext[..12]); + self.cipher.decrypt(nonce, &ciphertext[12..]) + } +} + +/// Derive session key from shared secret using HKDF-like expansion +pub fn derive_session_key(shared: &SharedSecret, salt: &[u8]) -> [u8; 32] { + let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(salt).expect("HMAC key size"); + mac.update(shared.as_bytes()); + mac.finalize().into_bytes().into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_psk_sign_verify() { + let psk = [0x42u8; 32]; + let validator = PskValidator::new(psk); + let data = b"hello"; + let sig = validator.sign(data); + assert!(validator.verify(data, &sig)); + } + + #[test] + fn test_key_exchange() { + let alice = KeyExchange::new(); + let bob = KeyExchange::new(); + let alice_pub = *alice.public_key(); + let bob_pub = *bob.public_key(); + let alice_shared = alice.derive_shared(&bob_pub); + let bob_shared = bob.derive_shared(&alice_pub); + assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); + } + + #[test] + fn test_aead_encrypt_decrypt() { + let key = [0x11u8; 32]; + let mut cipher = AeadCipher::new(&key); + let plaintext = b"secret message"; + let ciphertext = cipher.encrypt(plaintext).unwrap(); + let decrypted = cipher.decrypt(&ciphertext).unwrap(); + assert_eq!(plaintext.as_slice(), decrypted.as_slice()); + } +} diff --git a/ostp/src/lib.rs b/ostp/src/lib.rs new file mode 100644 index 0000000..b23b7cb --- /dev/null +++ b/ostp/src/lib.rs @@ -0,0 +1,11 @@ +pub mod client; +pub mod crypto; +pub mod mimicry; +pub mod server; +pub mod uot; + +pub use client::{ClientConfig, OstpClient}; +pub use crypto::{AeadCipher, KeyExchange, PskValidator}; +pub use mimicry::{MimicryEngine, TlsHelloBuilder}; +pub use server::{OstpServer, ServerConfig}; +pub use uot::{decode_frame, encode_frame, FrameFlags, Fragmenter, Reassembler}; diff --git a/ostp/src/mimicry.rs b/ostp/src/mimicry.rs new file mode 100644 index 0000000..14e7438 --- /dev/null +++ b/ostp/src/mimicry.rs @@ -0,0 +1,174 @@ +//! Dynamic SNI & TLS Mimicry Engine + +use std::collections::HashMap; + +/// Geo-based SNI target selection +pub struct MimicryEngine { + geo_sni_map: HashMap>, +} + +impl MimicryEngine { + pub fn new() -> Self { + let mut geo_sni_map = HashMap::new(); + + // Default geo-SNI mappings for contextual mimicry + geo_sni_map.insert( + "RU".into(), + vec![ + "gosuslugi.ru".into(), + "sberbank.ru".into(), + "yandex.ru".into(), + ], + ); + geo_sni_map.insert( + "NO".into(), + vec!["bankid.no".into(), "vipps.no".into(), "altinn.no".into()], + ); + geo_sni_map.insert( + "DE".into(), + vec![ + "sparkasse.de".into(), + "deutsche-bank.de".into(), + "bund.de".into(), + ], + ); + geo_sni_map.insert( + "US".into(), + vec![ + "apple.com".into(), + "microsoft.com".into(), + "amazon.com".into(), + ], + ); + geo_sni_map.insert( + "CN".into(), + vec!["qq.com".into(), "baidu.com".into(), "taobao.com".into()], + ); + + Self { geo_sni_map } + } + + /// Select SNI based on geo-location + pub fn select_sni(&self, country_code: &str) -> Option<&str> { + self.geo_sni_map + .get(country_code) + .and_then(|list| list.first().map(|s| s.as_str())) + } + + /// Get random SNI from geo list for anti-fingerprinting + pub fn random_sni(&self, country_code: &str) -> Option<&str> { + self.geo_sni_map.get(country_code).and_then(|list| { + if list.is_empty() { + None + } else { + let idx = rand::random::() % list.len(); + Some(list[idx].as_str()) + } + }) + } + + /// Add custom geo-SNI mapping + pub fn add_mapping(&mut self, country: String, domains: Vec) { + self.geo_sni_map.insert(country, domains); + } +} + +impl Default for MimicryEngine { + fn default() -> Self { + Self::new() + } +} + +/// TLS ClientHello builder for REALITY-like mimicry +pub struct TlsHelloBuilder { + sni: String, + random_session_id: bool, +} + +impl TlsHelloBuilder { + pub fn new(sni: impl Into) -> Self { + Self { + sni: sni.into(), + random_session_id: true, + } + } + + /// Build minimal TLS 1.3 ClientHello-like header + pub fn build(&self) -> Vec { + let mut hello = Vec::with_capacity(256); + + // TLS record header + hello.push(0x16); // Handshake + hello.extend_from_slice(&[0x03, 0x01]); // TLS 1.0 for compat + + // Placeholder for length (will update) + let len_pos = hello.len(); + hello.extend_from_slice(&[0x00, 0x00]); + + // Handshake header + hello.push(0x01); // ClientHello + let hs_len_pos = hello.len(); + hello.extend_from_slice(&[0x00, 0x00, 0x00]); // length placeholder + + // Client version (TLS 1.2 presented, 1.3 in extensions) + hello.extend_from_slice(&[0x03, 0x03]); + + // Random (32 bytes) + let random: [u8; 32] = rand::random(); + hello.extend_from_slice(&random); + + // Session ID (32 bytes if random) + if self.random_session_id { + hello.push(32); + let session_id: [u8; 32] = rand::random(); + hello.extend_from_slice(&session_id); + } else { + hello.push(0); + } + + // Cipher suites (TLS 1.3 suites) + hello.extend_from_slice(&[0x00, 0x06]); // 3 suites + hello.extend_from_slice(&[0x13, 0x01]); // TLS_AES_128_GCM_SHA256 + hello.extend_from_slice(&[0x13, 0x02]); // TLS_AES_256_GCM_SHA384 + hello.extend_from_slice(&[0x13, 0x03]); // TLS_CHACHA20_POLY1305_SHA256 + + // Compression (null) + hello.extend_from_slice(&[0x01, 0x00]); + + // Extensions with SNI + let ext_start = hello.len(); + hello.extend_from_slice(&[0x00, 0x00]); // ext length placeholder + + // SNI extension + hello.extend_from_slice(&[0x00, 0x00]); // type: server_name + let sni_bytes = self.sni.as_bytes(); + let sni_ext_len = sni_bytes.len() + 5; + hello.extend_from_slice(&(sni_ext_len as u16).to_be_bytes()); + hello.extend_from_slice(&((sni_bytes.len() + 3) as u16).to_be_bytes()); + hello.push(0x00); // host_name type + hello.extend_from_slice(&(sni_bytes.len() as u16).to_be_bytes()); + hello.extend_from_slice(sni_bytes); + + // supported_versions extension + hello.extend_from_slice(&[0x00, 0x2b]); // type + hello.extend_from_slice(&[0x00, 0x03]); // length + hello.push(0x02); // versions length + hello.extend_from_slice(&[0x03, 0x04]); // TLS 1.3 + + // Update lengths + let ext_len = hello.len() - ext_start - 2; + hello[ext_start] = (ext_len >> 8) as u8; + hello[ext_start + 1] = ext_len as u8; + + let total_len = hello.len() - 5; + hello[len_pos] = (total_len >> 8) as u8; + hello[len_pos + 1] = total_len as u8; + + let hs_len = hello.len() - hs_len_pos - 3; + hello[hs_len_pos] = 0; + hello[hs_len_pos + 1] = (hs_len >> 8) as u8; + hello[hs_len_pos + 2] = hs_len as u8; + + hello + } +} diff --git a/ostp/src/server.rs b/ostp/src/server.rs new file mode 100644 index 0000000..03a5d02 --- /dev/null +++ b/ostp/src/server.rs @@ -0,0 +1,192 @@ +//! OSTP Server - Stealth relay endpoint + +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::RwLock; + +use crate::crypto::{AeadCipher, KeyExchange, PskValidator}; +use crate::uot::decode_frame; +use bytes::BytesMut; + +/// Server configuration +#[derive(Clone)] +pub struct ServerConfig { + pub listen_addr: SocketAddr, + pub psk: [u8; 32], + pub max_connections: usize, +} + +impl ServerConfig { + pub fn new(addr: SocketAddr, psk: [u8; 32]) -> Self { + Self { + listen_addr: addr, + psk, + max_connections: 1024, + } + } +} + +/// Active connection state +#[allow(dead_code)] +struct Connection { + cipher: AeadCipher, + user_id: Option, + bytes_rx: u64, + bytes_tx: u64, +} + +/// OSTP Server with silent handshake +pub struct OstpServer { + config: ServerConfig, + #[allow(dead_code)] + psk_validator: PskValidator, + connections: Arc>>, +} + +impl OstpServer { + pub fn new(config: ServerConfig) -> Self { + let psk_validator = PskValidator::new(config.psk); + Self { + config, + psk_validator, + connections: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Start the server (main async loop) + pub async fn run(&self) -> anyhow::Result<()> { + let listener = TcpListener::bind(self.config.listen_addr).await?; + tracing::info!("OSTP server listening on {}", self.config.listen_addr); + + loop { + match listener.accept().await { + Ok((stream, addr)) => { + let psk_validator = PskValidator::new(self.config.psk); + let connections = Arc::clone(&self.connections); + + tokio::spawn(async move { + if let Err(e) = Self::handle_connection(stream, addr, psk_validator, connections).await { + tracing::debug!("Connection {} closed: {}", addr, e); + } + }); + } + Err(e) => { + tracing::warn!("Accept error: {}", e); + } + } + } + } + + async fn handle_connection( + mut stream: TcpStream, + addr: SocketAddr, + psk_validator: PskValidator, + connections: Arc>>, + ) -> anyhow::Result<()> { + let mut buf = [0u8; 4096]; + + // Phase 1: Silent PSK handshake (32-byte signature must be first) + let n = stream.read(&mut buf).await?; + if n < 64 { + // Silent drop - don't reveal port is open + return Ok(()); + } + + let signature: [u8; 32] = buf[..32].try_into()?; + let payload = &buf[32..n]; + + if !psk_validator.verify(payload, &signature) { + // Silent drop on invalid PSK - core anti-probing defense + tracing::trace!("Silent drop for {}: invalid PSK", addr); + return Ok(()); + } + + // Phase 2: X25519 key exchange + // Payload contains client's public key (32 bytes) + if payload.len() < 32 { + return Ok(()); + } + + let client_pubkey_bytes: [u8; 32] = payload[..32].try_into()?; + let client_pubkey = x25519_dalek::PublicKey::from(client_pubkey_bytes); + + let server_kex = KeyExchange::new(); + let server_pubkey = *server_kex.public_key(); + let shared_secret = server_kex.derive_shared(&client_pubkey); + + // Derive session key + let session_key = crate::crypto::derive_session_key(&shared_secret, b"ostp-session-v1"); + let cipher = AeadCipher::new(&session_key); + + // Send server's public key (signed with PSK) + let server_response = server_pubkey.as_bytes(); + let response_sig = psk_validator.sign(server_response); + stream.write_all(&response_sig).await?; + stream.write_all(server_response).await?; + + // Store connection + { + let mut conns = connections.write().await; + conns.insert(addr, Connection { + cipher, + user_id: None, + bytes_rx: 0, + bytes_tx: 0, + }); + } + + tracing::info!("Session established with {}", addr); + + // Phase 3: Encrypted relay loop + let mut read_buf = BytesMut::with_capacity(65536); + + loop { + let n = stream.read(&mut buf).await?; + if n == 0 { + break; + } + + read_buf.extend_from_slice(&buf[..n]); + + // Process frames + while let Some((data, flags)) = decode_frame(&mut read_buf)? { + let conns = connections.read().await; + if let Some(conn) = conns.get(&addr) { + match conn.cipher.decrypt(&data) { + Ok(plaintext) => { + if flags.is_data() { + // TODO: Route decrypted data to destination + tracing::trace!("Received {} bytes from {}", plaintext.len(), addr); + } + } + Err(_) => { + tracing::warn!("Decrypt failed for {}", addr); + break; + } + } + } + } + } + + // Cleanup + connections.write().await.remove(&addr); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_server_config() { + let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap(); + let psk = [0x42u8; 32]; + let config = ServerConfig::new(addr, psk); + assert_eq!(config.listen_addr, addr); + assert_eq!(config.max_connections, 1024); + } +} diff --git a/ostp/src/uot.rs b/ostp/src/uot.rs new file mode 100644 index 0000000..788945a --- /dev/null +++ b/ostp/src/uot.rs @@ -0,0 +1,132 @@ +//! UoT (UDP-over-TCP) framing layer + +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use thiserror::Error; + +/// Maximum UDP datagram size +pub const MAX_UDP_SIZE: usize = 65535; +/// Frame header: 2 bytes length + 1 byte flags +const HEADER_SIZE: usize = 3; + +#[derive(Error, Debug)] +pub enum FrameError { + #[error("frame too large: {0} bytes")] + TooLarge(usize), + #[error("incomplete frame")] + Incomplete, + #[error("invalid frame")] + Invalid, +} + +#[derive(Clone, Copy, Debug)] +pub struct FrameFlags(u8); + +impl FrameFlags { + pub const DATA: Self = Self(0x00); + pub const CONTROL: Self = Self(0x01); + pub const PADDING: Self = Self(0x02); + + pub fn is_data(self) -> bool { + self.0 == 0x00 + } +} + +/// Encapsulate UDP datagram into framed format with random padding +pub fn encode_frame(data: &[u8], flags: FrameFlags, padding: usize) -> Result { + let total_len = data.len() + padding; + if total_len > MAX_UDP_SIZE { + return Err(FrameError::TooLarge(total_len)); + } + + let mut buf = BytesMut::with_capacity(HEADER_SIZE + total_len); + buf.put_u16(total_len as u16); + buf.put_u8(flags.0); + buf.put_slice(data); + + // Random padding to avoid fingerprinting + if padding > 0 { + let pad: Vec = (0..padding).map(|_| rand::random()).collect(); + buf.put_slice(&pad); + } + + Ok(buf.freeze()) +} + +/// Decode frame from buffer, returns (payload, data_length, flags) +pub fn decode_frame(buf: &mut BytesMut) -> Result, FrameError> { + if buf.len() < HEADER_SIZE { + return Ok(None); + } + + let len = u16::from_be_bytes([buf[0], buf[1]]) as usize; + let flags = FrameFlags(buf[2]); + + if buf.len() < HEADER_SIZE + len { + return Ok(None); + } + + buf.advance(HEADER_SIZE); + let data = buf.split_to(len).freeze(); + + Ok(Some((data, flags))) +} + +/// MTU-aware fragmentation +pub struct Fragmenter { + mtu: usize, +} + +impl Fragmenter { + pub fn new(mtu: usize) -> Self { + Self { mtu } + } + + pub fn fragment(&self, data: &[u8]) -> Vec { + data.chunks(self.mtu) + .map(|chunk| Bytes::copy_from_slice(chunk)) + .collect() + } +} + +/// Reassembly buffer for fragmented packets +pub struct Reassembler { + buffer: BytesMut, + expected_len: Option, +} + +impl Reassembler { + pub fn new() -> Self { + Self { + buffer: BytesMut::new(), + expected_len: None, + } + } + + pub fn push(&mut self, fragment: &[u8], total_len: usize) { + if self.expected_len.is_none() { + self.expected_len = Some(total_len); + } + self.buffer.extend_from_slice(fragment); + } + + pub fn is_complete(&self) -> bool { + self.expected_len + .map(|len| self.buffer.len() >= len) + .unwrap_or(false) + } + + pub fn take(&mut self) -> Option { + if self.is_complete() { + self.expected_len = None; + Some(self.buffer.split().freeze()) + } else { + None + } + } +} + +impl Default for Reassembler { + fn default() -> Self { + Self::new() + } +}