Put user accounts to separated config file.

This commit is contained in:
Quintus Leung
2025-05-29 11:55:58 +01:00
parent 762674b8d5
commit bfbdfd8312
10 changed files with 980 additions and 61 deletions
Generated
+444 -1
View File
@@ -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"
+4
View File
@@ -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"
+2 -6
View File
@@ -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:
+38 -35
View File
@@ -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<UserCredentials>,
pub user_config_file: Option<String>,
}
#[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<P: AsRef<Path>>(&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::<std::net::IpAddr>()
.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::<std::net::IpAddr>()
.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<SocketAddr> {
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<SocketAddr> {
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<bool> {
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<Option<UserConfig>> {
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
})
}
}
+200
View File
@@ -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<String, UserEntry>,
}
#[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<String>,
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<P: AsRef<Path>>(path: P) -> Result<Self> {
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<P: AsRef<Path>>(&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<String>)> {
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<String>)> {
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<String>)> {
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<String>)> {
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<String>) -> bool {
use scrypt::Scrypt;
if let Ok(parsed_hash) = PasswordHash::new(hash) {
Scrypt.verify_password(password.as_bytes(), &parsed_hash).is_ok()
} else {
false
}
}
}
+1 -1
View File
@@ -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;
+250 -13
View File
@@ -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::<String>("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::<String>("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<WorkerGu
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(log_level.to_string()));
let registry = tracing_subscriber::registry().with(filter);
match (&config.logging.file, config.logging.console) {
@@ -131,12 +226,12 @@ fn setup_logging(config: &Config, matches: &clap::ArgMatches) -> Option<WorkerGu
std::path::Path::new(log_file).file_name().unwrap()
);
let (non_blocking_file, guard) = tracing_appender::non_blocking(file_appender);
registry
.with(fmt::layer().with_writer(std::io::stdout))
.with(fmt::layer().with_writer(non_blocking_file).with_ansi(false))
.init();
Some(guard)
Some(guard)
},
(Some(log_file), false) => {
let file_appender = tracing_appender::rolling::never(
@@ -144,11 +239,11 @@ fn setup_logging(config: &Config, matches: &clap::ArgMatches) -> Option<WorkerGu
std::path::Path::new(log_file).file_name().unwrap()
);
let (non_blocking_file, guard) = tracing_appender::non_blocking(file_appender);
registry
.with(fmt::layer().with_writer(non_blocking_file).with_ansi(false))
.init();
Some(guard)
Some(guard)
},
(None, true) => {
registry
@@ -157,7 +252,149 @@ fn setup_logging(config: &Config, matches: &clap::ArgMatches) -> Option<WorkerGu
None
},
(None, false) => {
None
None
}
}
}
fn handle_user_command(matches: &ArgMatches) -> Result<()> {
let user_config_path = matches.get_one::<String>("user-config").unwrap();
match matches.subcommand() {
Some(("init", sub_matches)) => {
let hash_type = parse_hash_type(sub_matches.get_one::<String>("hash-type").unwrap())?;
init_user_config(user_config_path, hash_type)?;
}
Some(("add", sub_matches)) => {
let username = sub_matches.get_one::<String>("username").unwrap();
let password = get_password_from_args_or_prompt(sub_matches, "password")?;
let hash_type = parse_hash_type(sub_matches.get_one::<String>("hash-type").unwrap())?;
add_user(user_config_path, username, &password, hash_type)?;
}
Some(("remove", sub_matches)) => {
let username = sub_matches.get_one::<String>("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::<String>("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::<String>("username").unwrap();
let enabled = sub_matches.get_one::<String>("enabled").unwrap().parse::<bool>()?;
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<HashType> {
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<String> {
if let Some(password) = matches.get_one::<String>(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(())
}
+1 -1
View File
@@ -149,7 +149,7 @@ impl ProxyServer {
}
async fn handle_socks5_connection(mut stream: TcpStream, config: Arc<Config>) -> 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? {
+18 -4
View File
@@ -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<Config>,
}
impl Socks5Handler {
pub fn new(config: Arc<Config>) -> Self {
Self { config }
}
pub async fn handle_handshake<T>(&self, stream: &mut T, auth_required: bool) -> Result<bool>
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<T>(&self, stream: &mut T) -> Result<Socks5Request>
+22
View File
@@ -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 <username>
# To list users, use: rusty-socks user list
# To remove users, use: rusty-socks user remove <username>