From bfbdfd8312afbf6a290aa327433311612bb791ae Mon Sep 17 00:00:00 2001 From: Quintus Leung Date: Thu, 29 May 2025 11:55:58 +0100 Subject: [PATCH] Put user accounts to separated config file. --- Cargo.lock | 445 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 + config.yml.example | 8 +- src/config.rs | 73 ++++--- src/config/user_config.rs | 200 +++++++++++++++++ src/lib.rs | 2 +- src/main.rs | 263 ++++++++++++++++++++-- src/server.rs | 2 +- src/socks5.rs | 22 +- users.yml.example | 22 ++ 10 files changed, 980 insertions(+), 61 deletions(-) create mode 100644 src/config/user_config.rs create mode 100644 users.yml.example diff --git a/Cargo.lock b/Cargo.lock index f19fc38..3057060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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.18" @@ -82,6 +97,18 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -142,24 +169,123 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + +[[package]] +name = "bcrypt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cc" +version = "1.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "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", +] + [[package]] name = "clap" version = "4.5.38" @@ -206,6 +332,21 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[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 = "crossbeam-channel" version = "0.5.15" @@ -221,6 +362,16 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -236,6 +387,17 @@ dependencies = [ "powerfmt", ] +[[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 = "displaydoc" version = "0.2.5" @@ -363,6 +525,16 @@ dependencies = [ "slab", ] +[[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" @@ -392,6 +564,39 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[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.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +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 = "icu_collections" version = "2.0.0" @@ -519,6 +724,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -549,6 +763,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -649,6 +873,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -699,6 +932,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -854,17 +1108,27 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "rusty-socks" version = "0.1.0" dependencies = [ "anyhow", + "argon2", "async-trait", - "base64", + "base64 0.21.7", + "bcrypt", "bytes", + "chrono", "clap", "futures", "rand", + "scrypt", "serde", "serde_yaml", "socket2", @@ -884,12 +1148,33 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "serde" version = "1.0.219" @@ -923,6 +1208,17 @@ dependencies = [ "unsafe-libyaml", ] +[[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" @@ -932,6 +1228,12 @@ dependencies = [ "lazy_static", ] +[[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.5" @@ -978,6 +1280,12 @@ 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.101" @@ -1271,6 +1579,12 @@ dependencies = [ "trust-dns-proto", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -1327,12 +1641,76 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[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.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "widestring" version = "1.2.0" @@ -1361,6 +1739,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1590,6 +2027,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 1038e4e..9daa350 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,10 @@ trust-dns-resolver = "0.23" base64 = "0.21" futures = "0.3" rand = "0.8" +argon2 = "0.5" +bcrypt = "0.15" +scrypt = "0.11" +chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] tokio-test = "0.4" diff --git a/config.yml.example b/config.yml.example index 523a485..354f329 100644 --- a/config.yml.example +++ b/config.yml.example @@ -28,12 +28,8 @@ auth: # Authentication method: "none" or "username_password" method: "none" - # User credentials (only used if auth is enabled) - users: - - username: "user1" - password: "password1" - - username: "admin" - password: "secure_password" + # Path to user configuration file (required if auth is enabled) + user_config_file: "users.yml" # Logging configuration logging: diff --git a/src/config.rs b/src/config.rs index 6043b27..22807e5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,9 @@ use std::net::SocketAddr; use std::path::Path; use tracing::info; +mod user_config; +pub use user_config::{UserConfig, HashType}; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub server: ServerConfig, @@ -26,7 +29,7 @@ pub struct ServerConfig { pub struct AuthConfig { pub enabled: bool, pub method: AuthMethod, - pub users: Vec, + pub user_config_file: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -37,11 +40,6 @@ pub enum AuthMethod { UsernamePassword, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UserCredentials { - pub username: String, - pub password: String, -} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggingConfig { @@ -78,7 +76,7 @@ impl Default for Config { auth: AuthConfig { enabled: false, method: AuthMethod::None, - users: vec![], + user_config_file: None, }, logging: LoggingConfig { level: "info".to_string(), @@ -103,78 +101,83 @@ impl Config { info!("Configuration loaded successfully"); Ok(config) } - + pub fn save_to_file>(&self, path: P) -> Result<()> { let content = serde_yaml::to_string(self)?; std::fs::write(path, content)?; Ok(()) } - + pub fn validate(&self) -> Result<()> { if self.server.socks5_port == 0 { return Err(anyhow!("Invalid SOCKS5 port: {}", self.server.socks5_port)); } - + if self.server.http_port == 0 { return Err(anyhow!("Invalid HTTP port: {}", self.server.http_port)); } - + if self.server.socks5_port == self.server.http_port { return Err(anyhow!("SOCKS5 and HTTP ports cannot be the same")); } - + self.server.bind_address.parse::() .map_err(|_| anyhow!("Invalid bind address: {}", self.server.bind_address))?; - + if self.server.max_connections == 0 { return Err(anyhow!("Max connections must be greater than 0")); } - + if self.server.buffer_size < 1024 { return Err(anyhow!("Buffer size must be at least 1024 bytes")); } - - if self.auth.enabled && self.auth.users.is_empty() { - return Err(anyhow!("Authentication enabled but no users configured")); + + if self.auth.enabled && self.auth.user_config_file.is_none() { + return Err(anyhow!("Authentication enabled but no user config file specified")); } - - for user in &self.auth.users { - if user.username.is_empty() || user.password.is_empty() { - return Err(anyhow!("Username and password cannot be empty")); - } - } - + for network in &self.security.allowed_networks { if !network.contains('/') { network.parse::() .map_err(|_| anyhow!("Invalid network address: {}", network))?; } } - + if !["trace", "debug", "info", "warn", "error"].contains(&self.logging.level.as_str()) { return Err(anyhow!("Invalid log level: {}", self.logging.level)); } - + Ok(()) } - + pub fn socks5_bind_addr(&self) -> Result { let addr = format!("{}:{}", self.server.bind_address, self.server.socks5_port); addr.parse().map_err(|e| anyhow!("Failed to parse SOCKS5 bind address: {}", e)) } - + pub fn http_bind_addr(&self) -> Result { let addr = format!("{}:{}", self.server.bind_address, self.server.http_port); addr.parse().map_err(|e| anyhow!("Failed to parse HTTP bind address: {}", e)) } - - pub fn validate_user(&self, username: &str, password: &str) -> bool { + + pub fn validate_user(&self, username: &str, password: &str) -> Result { if !self.auth.enabled { - return true; + return Ok(true); + } + + if let Some(user_config_path) = &self.auth.user_config_file { + let user_config = UserConfig::load_from_file(user_config_path)?; + Ok(user_config.verify_password(username, password)) + } else { + Err(anyhow!("Authentication enabled but no user config file specified")) + } + } + + pub fn load_user_config(&self) -> Result> { + if let Some(user_config_path) = &self.auth.user_config_file { + Ok(Some(UserConfig::load_from_file(user_config_path)?)) + } else { + Ok(None) } - - self.auth.users.iter().any(|user| { - user.username == username && user.password == password - }) } } \ No newline at end of file diff --git a/src/config/user_config.rs b/src/config/user_config.rs new file mode 100644 index 0000000..021c8a0 --- /dev/null +++ b/src/config/user_config.rs @@ -0,0 +1,200 @@ +use anyhow::{anyhow, Result}; +use argon2::{PasswordHash, PasswordHasher, PasswordVerifier}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; +use tracing::info; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserConfig { + pub hash_type: HashType, + pub users: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HashType { + #[serde(rename = "argon2")] + Argon2, + #[serde(rename = "bcrypt")] + Bcrypt, + #[serde(rename = "scrypt")] + Scrypt, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserEntry { + pub password_hash: String, + pub salt: Option, + pub created_at: String, + pub last_modified: String, + pub enabled: bool, +} + +impl Default for UserConfig { + fn default() -> Self { + Self { + hash_type: HashType::Argon2, + users: HashMap::new(), + } + } +} + +impl UserConfig { + pub fn load_from_file>(path: P) -> Result { + let content = std::fs::read_to_string(path)?; + let config: UserConfig = serde_yaml::from_str(&content)?; + config.validate()?; + info!("User configuration loaded successfully"); + Ok(config) + } + + pub fn save_to_file>(&self, path: P) -> Result<()> { + let content = serde_yaml::to_string(self)?; + std::fs::write(path, content)?; + Ok(()) + } + + pub fn validate(&self) -> Result<()> { + for (username, user) in &self.users { + if username.is_empty() { + return Err(anyhow!("Username cannot be empty")); + } + if user.password_hash.is_empty() { + return Err(anyhow!("Password hash cannot be empty for user: {}", username)); + } + } + Ok(()) + } + + pub fn add_user(&mut self, username: String, password: &str) -> Result<()> { + if self.users.contains_key(&username) { + return Err(anyhow!("User already exists: {}", username)); + } + + let (password_hash, salt) = self.hash_password(password)?; + let now = chrono::Utc::now().to_rfc3339(); + + let user_entry = UserEntry { + password_hash, + salt, + created_at: now.clone(), + last_modified: now, + enabled: true, + }; + + self.users.insert(username, user_entry); + Ok(()) + } + + pub fn remove_user(&mut self, username: &str) -> Result<()> { + if !self.users.contains_key(username) { + return Err(anyhow!("User not found: {}", username)); + } + self.users.remove(username); + Ok(()) + } + + pub fn update_password(&mut self, username: &str, new_password: &str) -> Result<()> { + let (password_hash, salt) = self.hash_password(new_password)?; + + let user = self.users.get_mut(username) + .ok_or_else(|| anyhow!("User not found: {}", username))?; + + user.password_hash = password_hash; + user.salt = salt; + user.last_modified = chrono::Utc::now().to_rfc3339(); + + Ok(()) + } + + pub fn enable_user(&mut self, username: &str, enabled: bool) -> Result<()> { + let user = self.users.get_mut(username) + .ok_or_else(|| anyhow!("User not found: {}", username))?; + + user.enabled = enabled; + user.last_modified = chrono::Utc::now().to_rfc3339(); + + Ok(()) + } + + pub fn verify_password(&self, username: &str, password: &str) -> bool { + if let Some(user) = self.users.get(username) { + if !user.enabled { + return false; + } + + match self.hash_type { + HashType::Argon2 => self.verify_argon2_password(password, &user.password_hash), + HashType::Bcrypt => self.verify_bcrypt_password(password, &user.password_hash), + HashType::Scrypt => self.verify_scrypt_password(password, &user.password_hash, &user.salt), + } + } else { + false + } + } + + fn hash_password(&self, password: &str) -> Result<(String, Option)> { + match self.hash_type { + HashType::Argon2 => self.hash_argon2_password(password), + HashType::Bcrypt => self.hash_bcrypt_password(password), + HashType::Scrypt => self.hash_scrypt_password(password), + } + } + + fn hash_argon2_password(&self, password: &str) -> Result<(String, Option)> { + use argon2::Argon2; + use argon2::password_hash::{SaltString, rand_core::OsRng}; + + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2.hash_password(password.as_bytes(), &salt) + .map_err(|e| anyhow!("Failed to hash password: {}", e))?; + + Ok((password_hash.to_string(), None)) + } + + fn verify_argon2_password(&self, password: &str, hash: &str) -> bool { + use argon2::Argon2; + + if let Ok(parsed_hash) = PasswordHash::new(hash) { + Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok() + } else { + false + } + } + + fn hash_bcrypt_password(&self, password: &str) -> Result<(String, Option)> { + use bcrypt::{hash, DEFAULT_COST}; + + let hash = hash(password, DEFAULT_COST) + .map_err(|e| anyhow!("Failed to hash password: {}", e))?; + + Ok((hash, None)) + } + + fn verify_bcrypt_password(&self, password: &str, hash: &str) -> bool { + use bcrypt::verify; + verify(password, hash).unwrap_or(false) + } + + fn hash_scrypt_password(&self, password: &str) -> Result<(String, Option)> { + use scrypt::Scrypt; + use scrypt::password_hash::{SaltString, rand_core::OsRng}; + + let salt = SaltString::generate(&mut OsRng); + let password_hash = Scrypt.hash_password(password.as_bytes(), &salt) + .map_err(|e| anyhow!("Failed to hash password: {}", e))?; + + Ok((password_hash.to_string(), Some(salt.to_string()))) + } + + fn verify_scrypt_password(&self, password: &str, hash: &str, _salt: &Option) -> bool { + use scrypt::Scrypt; + + if let Ok(parsed_hash) = PasswordHash::new(hash) { + Scrypt.verify_password(password.as_bytes(), &parsed_hash).is_ok() + } else { + false + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7e3b590..5f15771 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,5 @@ pub mod http_proxy; pub mod server; pub mod socks5; -pub use config::Config; +pub use config::{Config, UserConfig, HashType}; pub use server::ProxyServer; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9ead602..8f15ffc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use anyhow::Result; -use clap::{Arg, Command}; -use rusty_socks::{Config, ProxyServer}; +use clap::{Arg, Command, ArgMatches}; +use rusty_socks::{Config, ProxyServer, UserConfig, HashType}; +use std::io::{self, Write}; use tracing::{error, info, Level}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; @@ -43,6 +44,95 @@ async fn main() -> Result<()> { .help("Suppress all output except errors") .conflicts_with("verbose"), ) + .subcommand( + Command::new("user") + .about("User management commands") + .arg( + Arg::new("user-config") + .long("user-config") + .value_name("FILE") + .help("User configuration file path") + .default_value("users.yml"), + ) + .subcommand( + Command::new("add") + .about("Add a new user") + .arg( + Arg::new("username") + .help("Username") + .required(true) + .index(1), + ) + .arg( + Arg::new("password") + .help("Password (will prompt if not provided)") + .index(2), + ) + .arg( + Arg::new("hash-type") + .long("hash-type") + .value_name("TYPE") + .help("Password hash type: argon2, bcrypt, scrypt") + .default_value("argon2"), + ), + ) + .subcommand( + Command::new("remove") + .about("Remove a user") + .arg( + Arg::new("username") + .help("Username to remove") + .required(true) + .index(1), + ), + ) + .subcommand( + Command::new("list") + .about("List all users"), + ) + .subcommand( + Command::new("update") + .about("Update user password") + .arg( + Arg::new("username") + .help("Username") + .required(true) + .index(1), + ) + .arg( + Arg::new("password") + .help("New password (will prompt if not provided)") + .index(2), + ), + ) + .subcommand( + Command::new("enable") + .about("Enable/disable a user") + .arg( + Arg::new("username") + .help("Username") + .required(true) + .index(1), + ) + .arg( + Arg::new("enabled") + .help("Enable (true) or disable (false)") + .required(true) + .index(2), + ), + ) + .subcommand( + Command::new("init") + .about("Initialize a new user configuration file") + .arg( + Arg::new("hash-type") + .long("hash-type") + .value_name("TYPE") + .help("Default password hash type: argon2, bcrypt, scrypt") + .default_value("argon2"), + ), + ), + ) .get_matches(); if let Some(config_path) = matches.get_one::("generate-config") { @@ -50,8 +140,13 @@ async fn main() -> Result<()> { return Ok(()); } + if let Some(user_matches) = matches.subcommand_matches("user") { + handle_user_command(user_matches)?; + return Ok(()); + } + let config_path = matches.get_one::("config").unwrap(); - + let config = if std::path::Path::new(config_path).exists() { match Config::load_from_file(config_path) { Ok(config) => config, @@ -62,7 +157,7 @@ async fn main() -> Result<()> { }; let _guard = setup_logging(&config, &matches); - + if std::path::Path::new(config_path).exists() { match Config::load_from_file(config_path) { Ok(_) => { @@ -82,7 +177,7 @@ async fn main() -> Result<()> { info!("HTTP proxy will listen on {}:{}", config.server.bind_address, config.server.http_port); let server = ProxyServer::new(config); - + if let Err(e) = server.start().await { error!("Server error: {}", e); std::process::exit(1); @@ -94,10 +189,10 @@ async fn main() -> Result<()> { fn generate_default_config(path: &str) -> Result<()> { let config = Config::default(); config.save_to_file(path)?; - + println!("Generated default configuration file: {}", path); println!("Edit this file to customize your proxy server settings."); - + Ok(()) } @@ -121,7 +216,7 @@ fn setup_logging(config: &Config, matches: &clap::ArgMatches) -> Option Option { let file_appender = tracing_appender::rolling::never( @@ -144,11 +239,11 @@ fn setup_logging(config: &Config, matches: &clap::ArgMatches) -> Option { registry @@ -157,7 +252,149 @@ fn setup_logging(config: &Config, matches: &clap::ArgMatches) -> Option { - None + None } } } + +fn handle_user_command(matches: &ArgMatches) -> Result<()> { + let user_config_path = matches.get_one::("user-config").unwrap(); + + match matches.subcommand() { + Some(("init", sub_matches)) => { + let hash_type = parse_hash_type(sub_matches.get_one::("hash-type").unwrap())?; + init_user_config(user_config_path, hash_type)?; + } + Some(("add", sub_matches)) => { + let username = sub_matches.get_one::("username").unwrap(); + let password = get_password_from_args_or_prompt(sub_matches, "password")?; + let hash_type = parse_hash_type(sub_matches.get_one::("hash-type").unwrap())?; + add_user(user_config_path, username, &password, hash_type)?; + } + Some(("remove", sub_matches)) => { + let username = sub_matches.get_one::("username").unwrap(); + remove_user(user_config_path, username)?; + } + Some(("list", _)) => { + list_users(user_config_path)?; + } + Some(("update", sub_matches)) => { + let username = sub_matches.get_one::("username").unwrap(); + let password = get_password_from_args_or_prompt(sub_matches, "password")?; + update_user_password(user_config_path, username, &password)?; + } + Some(("enable", sub_matches)) => { + let username = sub_matches.get_one::("username").unwrap(); + let enabled = sub_matches.get_one::("enabled").unwrap().parse::()?; + enable_user(user_config_path, username, enabled)?; + } + _ => { + eprintln!("No valid user subcommand provided. Use --help for usage information."); + } + } + + Ok(()) +} + +fn parse_hash_type(hash_type_str: &str) -> Result { + match hash_type_str.to_lowercase().as_str() { + "argon2" => Ok(HashType::Argon2), + "bcrypt" => Ok(HashType::Bcrypt), + "scrypt" => Ok(HashType::Scrypt), + _ => Err(anyhow::anyhow!("Invalid hash type: {}. Valid options are: argon2, bcrypt, scrypt", hash_type_str)), + } +} + +fn get_password_from_args_or_prompt(matches: &ArgMatches, arg_name: &str) -> Result { + if let Some(password) = matches.get_one::(arg_name) { + Ok(password.clone()) + } else { + print!("Enter password: "); + io::stdout().flush()?; + let mut password = String::new(); + io::stdin().read_line(&mut password)?; + Ok(password.trim().to_string()) + } +} + +fn init_user_config(path: &str, hash_type: HashType) -> Result<()> { + if std::path::Path::new(path).exists() { + return Err(anyhow::anyhow!("User config file already exists: {}", path)); + } + + let mut user_config = UserConfig::default(); + user_config.hash_type = hash_type; + user_config.save_to_file(path)?; + + println!("Initialized user configuration file: {}", path); + println!("Hash type: {:?}", user_config.hash_type); + + Ok(()) +} + +fn add_user(path: &str, username: &str, password: &str, hash_type: HashType) -> Result<()> { + let mut user_config = if std::path::Path::new(path).exists() { + UserConfig::load_from_file(path)? + } else { + let mut config = UserConfig::default(); + config.hash_type = hash_type; + config + }; + + user_config.add_user(username.to_string(), password)?; + user_config.save_to_file(path)?; + + println!("Added user: {}", username); + + Ok(()) +} + +fn remove_user(path: &str, username: &str) -> Result<()> { + let mut user_config = UserConfig::load_from_file(path)?; + user_config.remove_user(username)?; + user_config.save_to_file(path)?; + + println!("Removed user: {}", username); + + Ok(()) +} + +fn list_users(path: &str) -> Result<()> { + let user_config = UserConfig::load_from_file(path)?; + + println!("Hash type: {:?}", user_config.hash_type); + println!("Users:"); + + if user_config.users.is_empty() { + println!(" No users configured"); + } else { + for (username, user) in &user_config.users { + let status = if user.enabled { "enabled" } else { "disabled" }; + println!(" {} ({}) - created: {}, modified: {}", + username, status, user.created_at, user.last_modified); + } + } + + Ok(()) +} + +fn update_user_password(path: &str, username: &str, password: &str) -> Result<()> { + let mut user_config = UserConfig::load_from_file(path)?; + user_config.update_password(username, password)?; + user_config.save_to_file(path)?; + + println!("Updated password for user: {}", username); + + Ok(()) +} + +fn enable_user(path: &str, username: &str, enabled: bool) -> Result<()> { + let mut user_config = UserConfig::load_from_file(path)?; + user_config.enable_user(username, enabled)?; + user_config.save_to_file(path)?; + + let status = if enabled { "enabled" } else { "disabled" }; + println!("User {} {}", username, status); + + Ok(()) +} diff --git a/src/server.rs b/src/server.rs index 9d0e499..8a04266 100644 --- a/src/server.rs +++ b/src/server.rs @@ -149,7 +149,7 @@ impl ProxyServer { } async fn handle_socks5_connection(mut stream: TcpStream, config: Arc) -> Result<()> { - let handler = Socks5Handler; + let handler = Socks5Handler::new(config.clone()); let auth_required = config.auth.enabled; if !handler.handle_handshake(&mut stream, auth_required).await? { diff --git a/src/socks5.rs b/src/socks5.rs index 4082875..dbbaa0b 100644 --- a/src/socks5.rs +++ b/src/socks5.rs @@ -1,8 +1,11 @@ use anyhow::{anyhow, Result}; use bytes::{BufMut, BytesMut}; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::Arc; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tracing::{debug, trace}; +use tracing::{debug, trace, warn}; + +use crate::Config; #[derive(Debug, Clone, PartialEq)] pub enum AuthMethod { @@ -126,9 +129,14 @@ impl Socks5Response { } } -pub struct Socks5Handler; +pub struct Socks5Handler { + config: Arc, +} impl Socks5Handler { + pub fn new(config: Arc) -> Self { + Self { config } + } pub async fn handle_handshake(&self, stream: &mut T, auth_required: bool) -> Result where T: AsyncRead + AsyncWrite + Unpin, @@ -221,8 +229,14 @@ impl Socks5Handler { } } - fn validate_credentials(&self, _username: &str, _password: &str) -> bool { - true + fn validate_credentials(&self, username: &str, password: &str) -> bool { + match self.config.validate_user(username, password) { + Ok(valid) => valid, + Err(e) => { + warn!("Authentication error for user '{}': {}", username, e); + false + } + } } pub async fn handle_request(&self, stream: &mut T) -> Result diff --git a/users.yml.example b/users.yml.example new file mode 100644 index 0000000..8de73e3 --- /dev/null +++ b/users.yml.example @@ -0,0 +1,22 @@ +# User Configuration File for Rusty SOCKS Proxy +# This file contains user credentials with secure password hashing + +# Password hash type: argon2, bcrypt, or scrypt +hash_type: argon2 + +# User accounts +users: + # Example users (these are generated using secure hash functions) + # Use the 'rusty-socks user' commands to manage users + + # Sample user entry structure: + # username: + # password_hash: "$argon2id$v=19$m=65536,t=3,p=4$..." + # salt: null # Some hash types embed salt in the hash + # created_at: "2023-01-01T12:00:00Z" + # last_modified: "2023-01-01T12:00:00Z" + # enabled: true + + # To add users, use: rusty-socks user add + # To list users, use: rusty-socks user list + # To remove users, use: rusty-socks user remove \ No newline at end of file