Skip to content

Commit 47dc6e3

Browse files
committed
feat(cli): implement command line interface and app initialization
1 parent c358707 commit 47dc6e3

8 files changed

Lines changed: 487 additions & 4 deletions

File tree

crates/wind/Cargo.toml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,34 @@ name = "wind"
33
version.workspace = true
44
repository.workspace = true
55
edition.workspace = true
6-
description.workspace = true
7-
license = "AGPL-3.0-or-later"
6+
description = "A proxy tool written in Rust"
7+
license = "AGPL-3.0-or-later"
8+
9+
[dependencies]
10+
wind-core = { version = "0.1.1", path = "../wind-core"}
11+
wind-socks = { version = "0.1.1", path = "../wind-socks"}
12+
wind-tuic = { version = "0.1.1", path = "../wind-tuic"}
13+
14+
# Async
15+
tokio = { version = "1", features = ["rt-multi-thread", "signal", "net"] }
16+
tokio-util = { version = "0.7", features = ["rt"] }
17+
18+
tracing = "0.1"
19+
20+
clap = { version = "4", features = ["derive"] }
21+
clap_derive = { version = "4" }
22+
23+
time = { version = "0.3", features = ["macros", "local-offset"] }
24+
tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "std", "local-time","fmt", "ansi"] }
25+
const-str = "0.7"
26+
27+
eyre = "0.6"
28+
uuid = { version = "1", features = ["serde"] }
29+
30+
# Configuration
31+
figment = { version = "0.10", features = ["yaml", "env", "toml"] }
32+
serde = { version = "1", features = ["derive"] }
33+
serde_yaml = "0.9.34-deprecated"
34+
toml = "0.9"
35+
educe = { version = "0.6", features = ["Default"] }
36+
humantime-serde = "1"

crates/wind/src/cli.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use std::path::PathBuf;
2+
3+
use clap::{ArgAction, Parser, Subcommand};
4+
5+
#[derive(Parser)]
6+
#[command(about, long_about = None)]
7+
pub struct Cli {
8+
/// Set a custom config
9+
#[arg(short, visible_short_alias = 'f', long, value_name = "FILE/BASE64-TEXT")]
10+
pub config: Option<String>,
11+
12+
/// Set configuration directory
13+
#[arg(short = 'C', visible_short_alias = 'd', long, value_name = "PATH")]
14+
pub config_dir: Option<PathBuf>,
15+
16+
/// Set working directory
17+
#[arg(short = 'D', long, value_name = "PATH")]
18+
pub work_dir: Option<PathBuf>,
19+
20+
/// Show current version
21+
#[arg(short = 'v', visible_short_alias = 'V', long, action = ArgAction::SetTrue)]
22+
pub version: bool,
23+
24+
#[command(subcommand)]
25+
pub command: Option<Commands>,
26+
}
27+
28+
#[derive(Subcommand)]
29+
pub enum Commands {
30+
/// Initialize a new default configuration file
31+
Init {
32+
/// Specify the configuration file format (yaml or toml)
33+
#[arg(short, long, value_enum, default_value = "yaml")]
34+
format: ConfigFormat,
35+
},
36+
}
37+
38+
#[derive(clap::ValueEnum, Clone, Debug)]
39+
pub enum ConfigFormat {
40+
Yaml,
41+
Toml,
42+
}

crates/wind/src/conf/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod persistent;
2+
pub mod runtime;

crates/wind/src/conf/persistent.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use std::{
2+
net::{Ipv4Addr, SocketAddr},
3+
path::PathBuf,
4+
time::Duration,
5+
};
6+
7+
use educe::Educe;
8+
use figment::{
9+
Figment,
10+
providers::{Env, Format, Toml, Yaml},
11+
};
12+
use serde::{Deserialize, Serialize};
13+
use wind_core::types::TargetAddr;
14+
use wind_socks::inbound::AuthMode;
15+
16+
#[derive(Debug, Deserialize, Serialize, Educe)]
17+
#[educe(Default)]
18+
pub struct PersistentConfig {
19+
pub socks_opt: SocksOpt,
20+
pub tuic_opt: TuicOpt,
21+
}
22+
23+
#[derive(Debug, Deserialize, Serialize, Educe)]
24+
#[educe(Default)]
25+
pub struct SocksOpt {
26+
#[educe(Default(expression = "127.0.0.1:6666".parse().unwrap()))]
27+
pub listen_addr: SocketAddr,
28+
29+
#[educe(Default = None)]
30+
pub public_addr: Option<std::net::IpAddr>,
31+
32+
#[educe(Default = AuthModeConfig::NoAuth)]
33+
pub auth: AuthModeConfig,
34+
35+
#[educe(Default = false)]
36+
pub skip_auth: bool,
37+
38+
#[educe(Default = true)]
39+
pub allow_udp: bool,
40+
}
41+
42+
#[derive(Debug, Deserialize, Serialize, Clone, Educe)]
43+
#[educe(Default)]
44+
pub enum AuthModeConfig {
45+
#[educe(Default)]
46+
NoAuth,
47+
Password {
48+
username: String,
49+
password: String,
50+
},
51+
}
52+
53+
impl From<AuthModeConfig> for AuthMode {
54+
fn from(config: AuthModeConfig) -> Self {
55+
match config {
56+
AuthModeConfig::NoAuth => AuthMode::NoAuth,
57+
AuthModeConfig::Password { username, password } => AuthMode::Password { username, password },
58+
}
59+
}
60+
}
61+
62+
#[derive(Debug, Deserialize, Serialize, Educe)]
63+
#[educe(Default)]
64+
pub struct TuicOpt {
65+
#[educe(Default = TargetAddr::IPv4(Ipv4Addr::new(127, 0, 0, 1), 9443))]
66+
pub server_addr: TargetAddr,
67+
68+
#[educe(Default = "localhost")]
69+
pub sni: String,
70+
71+
#[educe(Default = "c1e6dbe2-f417-4890-994c-9ee15b926597".parse().unwrap())]
72+
pub uuid: uuid::Uuid,
73+
74+
#[educe(Default = "test_passwd")]
75+
pub password: String,
76+
77+
#[educe(Default = false)]
78+
pub zero_rtt_handshake: bool,
79+
80+
#[serde(with = "humantime_serde")]
81+
#[educe(Default(expression = Duration::from_secs(10)))]
82+
pub heartbeat: Duration,
83+
84+
#[serde(with = "humantime_serde")]
85+
#[educe(Default(expression = Duration::from_secs(20)))]
86+
pub gc_interval: Duration,
87+
88+
#[serde(with = "humantime_serde")]
89+
#[educe(Default(expression = Duration::from_secs(20)))]
90+
pub gc_lifetime: Duration,
91+
92+
#[educe(Default = true)]
93+
pub skip_cert_verify: bool,
94+
95+
#[educe(Default(expression = vec![String::from("h3")]))]
96+
pub alpn: Vec<String>,
97+
}
98+
99+
impl PersistentConfig {
100+
pub fn export_to_file(&self, file_path: &PathBuf, format: &str) -> eyre::Result<()> {
101+
use std::{fs, io::Write};
102+
103+
match format.to_lowercase().as_str() {
104+
"yaml" => {
105+
let yaml_content = serde_yaml::to_string(&self)?;
106+
let mut file = fs::File::create(file_path)?;
107+
file.write_all(yaml_content.as_bytes())?;
108+
}
109+
"toml" => {
110+
let toml_content = toml::to_string_pretty(&self)?;
111+
let mut file = fs::File::create(file_path)?;
112+
file.write_all(toml_content.as_bytes())?;
113+
}
114+
_ => return Err(eyre::eyre!("Unsupported file format: {}", format)),
115+
}
116+
117+
Ok(())
118+
}
119+
120+
pub fn load(config_path: Option<String>, config_dir: Option<PathBuf>) -> eyre::Result<Self> {
121+
// Start with empty figment (will use default values via serde)
122+
let mut figment = Figment::new();
123+
124+
// Load from default configuration location
125+
if let Some(config_dir) = config_dir {
126+
let config_file = config_dir.join("config.toml");
127+
if config_file.exists() {
128+
figment = figment.merge(Toml::file(config_file));
129+
}
130+
131+
let config_file = config_dir.join("config.yaml");
132+
if config_file.exists() {
133+
figment = figment.merge(Yaml::file(config_file));
134+
}
135+
} else {
136+
// Try to load from default locations in current directory
137+
let config_toml = std::path::Path::new("config.toml");
138+
if config_toml.exists() {
139+
figment = figment.merge(Toml::file(config_toml));
140+
}
141+
142+
let config_yaml = std::path::Path::new("config.yaml");
143+
if config_yaml.exists() {
144+
figment = figment.merge(Yaml::file(config_yaml));
145+
}
146+
}
147+
148+
// If specific config path is provided, use that
149+
if let Some(config_path) = config_path {
150+
if config_path.ends_with(".toml") {
151+
figment = figment.merge(Toml::file(config_path));
152+
} else if config_path.ends_with(".yaml") || config_path.ends_with(".yml") {
153+
figment = figment.merge(Yaml::file(config_path));
154+
} else {
155+
// Assume it's TOML format
156+
figment = figment.merge(Toml::file(config_path));
157+
}
158+
}
159+
160+
// Environment variables can override config files
161+
figment = figment.merge(Env::prefixed("WIND_"));
162+
163+
// Extract the configuration
164+
let config: PersistentConfig = figment.extract()?;
165+
166+
Ok(config)
167+
}
168+
}

crates/wind/src/conf/runtime.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use wind_socks::inbound::SocksInboundOpt;
2+
use wind_tuic::outbound::TuicOutboundOpts;
3+
4+
use crate::{conf::persistent::PersistentConfig, util::target_addr_to_socket_addr};
5+
6+
pub struct Config {
7+
pub socks_opt: SocksInboundOpt,
8+
pub tuic_opt: TuicOutboundOpts,
9+
}
10+
impl Config {
11+
pub fn from_persist(config: PersistentConfig) -> Self {
12+
Self {
13+
socks_opt: SocksInboundOpt {
14+
listen_addr: config.socks_opt.listen_addr,
15+
public_addr: config.socks_opt.public_addr,
16+
auth: config.socks_opt.auth.into(),
17+
skip_auth: config.socks_opt.skip_auth,
18+
allow_udp: config.socks_opt.allow_udp,
19+
},
20+
tuic_opt: TuicOutboundOpts {
21+
peer_addr: target_addr_to_socket_addr(&config.tuic_opt.server_addr),
22+
sni: config.tuic_opt.sni.clone(),
23+
auth: (config.tuic_opt.uuid, config.tuic_opt.password.as_bytes().to_vec().into()),
24+
zero_rtt_handshake: config.tuic_opt.zero_rtt_handshake,
25+
heartbeat: config.tuic_opt.heartbeat,
26+
gc_interval: config.tuic_opt.gc_interval,
27+
gc_lifetime: config.tuic_opt.gc_lifetime,
28+
skip_cert_verify: config.tuic_opt.skip_cert_verify,
29+
alpn: config.tuic_opt.alpn.clone(),
30+
},
31+
}
32+
}
33+
}

crates/wind/src/log.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use time::macros::format_description;
2+
use tracing::{Level, level_filters::LevelFilter};
3+
use tracing_subscriber::{fmt::time::LocalTime, layer::SubscriberExt as _, util::SubscriberInitExt as _};
4+
5+
pub fn init_log(level: Level) -> eyre::Result<()> {
6+
let filter = tracing_subscriber::filter::Targets::new()
7+
.with_targets(vec![
8+
("wind", level),
9+
("wind_core", level),
10+
("wind_tuic", level),
11+
("wind_socks", level),
12+
])
13+
.with_default(LevelFilter::INFO);
14+
let registry = tracing_subscriber::registry();
15+
registry
16+
.with(filter)
17+
.with(
18+
tracing_subscriber::fmt::layer()
19+
.with_target(true)
20+
.with_timer(LocalTime::new(format_description!(
21+
"[year repr:last_two]-[month]-[day] [hour]:[minute]:[second]"
22+
))),
23+
)
24+
.try_init()?;
25+
26+
Ok(())
27+
}

0 commit comments

Comments
 (0)