Refactor proxy routing and session management
This commit is contained in:
@@ -8,14 +8,16 @@ mod proxy_router;
|
||||
mod singbox;
|
||||
|
||||
use models::{
|
||||
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType, Protocol,
|
||||
ProxyProtocol, Target, TargetKind,
|
||||
ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig, Profile, ProfileItem,
|
||||
ProfileItemType, Protocol, ProxyProtocol, SubscriptionCache, SubscriptionServer, Target,
|
||||
TargetKind,
|
||||
};
|
||||
use proxifyre::{ProxiFyreAdapter, ProxiFyreConfig};
|
||||
use proxy_router::{ProxyRouterAdapter, ProxyRouterRequest};
|
||||
use singbox::{
|
||||
SingBoxAdapter, SingBoxCheckResult, SingBoxConfig, SingBoxConfigChecker, SingBoxConfigError,
|
||||
SingBoxConfigErrorKind, SingBoxGenerationRequest, SINGBOX_OUTPUT_FILE,
|
||||
SingBoxAdapter, SingBoxCheckResult, SingBoxConfigChecker, SingBoxConfigError,
|
||||
SingBoxConfigErrorKind, SingBoxGenerationRequest, DEFAULT_VPN_OUTBOUND_TAG,
|
||||
SINGBOX_OUTPUT_FILE,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -23,25 +25,25 @@ use std::{
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn generates_local_singbox_config_and_runs_check_when_binary_path_is_supplied() {
|
||||
fn generates_selected_outbound_config_and_runs_check_when_binary_path_is_supplied() {
|
||||
let adapter = SingBoxAdapter::default();
|
||||
let targets = vec![local_singbox_target()];
|
||||
let components = vec![running_singbox_component()];
|
||||
let config = local_singbox_config("nl-1");
|
||||
let cache = subscription_cache();
|
||||
let checker = RecordingChecker::ok("configuration OK");
|
||||
let binary_path = Path::new(r"C:\Tools\VpnProxy\sing-box\sing-box.exe");
|
||||
|
||||
let generated = adapter
|
||||
.generate_config(
|
||||
SingBoxGenerationRequest::new(&targets, &components, Some(binary_path)),
|
||||
SingBoxGenerationRequest::new(&config, &cache, Some(binary_path)),
|
||||
&checker,
|
||||
)
|
||||
.expect("running local sing-box should generate config");
|
||||
let config: SingBoxConfig =
|
||||
.expect("selected outbound should generate config");
|
||||
let generated_config: serde_json::Value =
|
||||
serde_json::from_str(&generated.contents).expect("generated sing-box json");
|
||||
|
||||
assert_eq!(generated.adapter_id, "singbox");
|
||||
assert_eq!(generated.output_file_name, SINGBOX_OUTPUT_FILE);
|
||||
assert_eq!(generated.local_target_id, "local-singbox");
|
||||
assert_eq!(generated.selected_server_tag, "nl-1");
|
||||
assert_eq!(generated.listen, "127.0.0.1");
|
||||
assert_eq!(generated.listen_port, 1080);
|
||||
assert_eq!(
|
||||
@@ -52,30 +54,39 @@ fn generates_local_singbox_config_and_runs_check_when_binary_path_is_supplied()
|
||||
message: "configuration OK".to_string(),
|
||||
})
|
||||
);
|
||||
assert_eq!(config.log.level, "info");
|
||||
assert_eq!(config.inbounds.len(), 1);
|
||||
assert_eq!(config.inbounds[0].inbound_type, "mixed");
|
||||
assert_eq!(config.inbounds[0].listen, "127.0.0.1");
|
||||
assert_eq!(config.inbounds[0].listen_port, 1080);
|
||||
assert!(!config.inbounds[0].set_system_proxy);
|
||||
assert_eq!(config.outbounds[0].outbound_type, "direct");
|
||||
assert_eq!(config.route.final_outbound, "direct");
|
||||
assert_eq!(generated_config["log"]["level"], "info");
|
||||
assert_eq!(generated_config["inbounds"][0]["type"], "mixed");
|
||||
assert_eq!(generated_config["inbounds"][0]["listen"], "127.0.0.1");
|
||||
assert_eq!(generated_config["inbounds"][0]["listen_port"], 1080);
|
||||
assert_eq!(generated_config["inbounds"][0]["set_system_proxy"], false);
|
||||
assert_eq!(generated_config["outbounds"][0]["type"], "vless");
|
||||
assert_eq!(
|
||||
generated_config["outbounds"][0]["tag"],
|
||||
DEFAULT_VPN_OUTBOUND_TAG
|
||||
);
|
||||
assert_eq!(
|
||||
generated_config["outbounds"][0]["server"],
|
||||
"nl.example.test"
|
||||
);
|
||||
assert_eq!(generated_config["outbounds"][0]["packet_encoding"], "xudp");
|
||||
assert_eq!(generated_config["route"]["final"], DEFAULT_VPN_OUTBOUND_TAG);
|
||||
let calls = checker.calls.borrow();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].0.as_path(), binary_path);
|
||||
assert!(calls[0].1.contains(r#""type": "mixed""#));
|
||||
assert!(calls[0].1.contains(r#""tag": "vpn""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_singbox_check_when_binary_path_is_not_supplied() {
|
||||
let adapter = SingBoxAdapter::default();
|
||||
let targets = vec![local_singbox_target()];
|
||||
let components = vec![running_singbox_component()];
|
||||
let config = local_singbox_config("nl-1");
|
||||
let cache = subscription_cache();
|
||||
let checker = RecordingChecker::ok("should not run");
|
||||
|
||||
let generated = adapter
|
||||
.generate_config(
|
||||
SingBoxGenerationRequest::new(&targets, &components, None),
|
||||
SingBoxGenerationRequest::new(&config, &cache, None),
|
||||
&checker,
|
||||
)
|
||||
.expect("binary path is optional");
|
||||
@@ -85,54 +96,53 @@ fn skips_singbox_check_when_binary_path_is_not_supplied() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_local_singbox_config_when_required_component_is_missing() {
|
||||
fn blocks_config_when_server_is_not_selected() {
|
||||
let adapter = SingBoxAdapter::default();
|
||||
let targets = vec![local_singbox_target()];
|
||||
let components = Vec::new();
|
||||
let mut config = local_singbox_config("nl-1");
|
||||
config.selected_server_tag = None;
|
||||
let cache = subscription_cache();
|
||||
let checker = RecordingChecker::ok("should not run");
|
||||
|
||||
let error = adapter
|
||||
.generate_config(
|
||||
SingBoxGenerationRequest::new(&targets, &components, None),
|
||||
SingBoxGenerationRequest::new(&config, &cache, None),
|
||||
&checker,
|
||||
)
|
||||
.expect_err("local sing-box target requires component state");
|
||||
.expect_err("missing selected server should block config");
|
||||
|
||||
assert_eq!(error.kind, SingBoxConfigErrorKind::MissingRequiredComponent);
|
||||
assert_eq!(error.kind, SingBoxConfigErrorKind::MissingSelectedServer);
|
||||
assert!(checker.calls.borrow().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_local_singbox_config_when_component_is_not_running() {
|
||||
fn blocks_config_when_selected_outbound_is_missing() {
|
||||
let adapter = SingBoxAdapter::default();
|
||||
let targets = vec![local_singbox_target()];
|
||||
let components = vec![stopped_singbox_component()];
|
||||
let config = local_singbox_config("missing-server");
|
||||
let cache = subscription_cache();
|
||||
let checker = RecordingChecker::ok("should not run");
|
||||
|
||||
let error = adapter
|
||||
.generate_config(
|
||||
SingBoxGenerationRequest::new(&targets, &components, None),
|
||||
SingBoxGenerationRequest::new(&config, &cache, None),
|
||||
&checker,
|
||||
)
|
||||
.expect_err("local sing-box target requires running component");
|
||||
.expect_err("missing outbound should block config");
|
||||
|
||||
assert_eq!(
|
||||
error.kind,
|
||||
SingBoxConfigErrorKind::RequiredComponentNotRunning
|
||||
);
|
||||
assert_eq!(error.kind, SingBoxConfigErrorKind::MissingSelectedOutbound);
|
||||
assert!(error.message.contains("missing-server"));
|
||||
assert!(checker.calls.borrow().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propagates_failed_singbox_check_as_structured_error() {
|
||||
let adapter = SingBoxAdapter::default();
|
||||
let targets = vec![local_singbox_target()];
|
||||
let components = vec![running_singbox_component()];
|
||||
let config = local_singbox_config("nl-1");
|
||||
let cache = subscription_cache();
|
||||
let checker = RecordingChecker::err("invalid config");
|
||||
|
||||
let error = adapter
|
||||
.generate_config(
|
||||
SingBoxGenerationRequest::new(&targets, &components, Some(Path::new("sing-box.exe"))),
|
||||
SingBoxGenerationRequest::new(&config, &cache, Some(Path::new("sing-box.exe"))),
|
||||
&checker,
|
||||
)
|
||||
.expect_err("failed sing-box check should block generated config");
|
||||
@@ -202,6 +212,46 @@ impl SingBoxConfigChecker for RecordingChecker {
|
||||
}
|
||||
}
|
||||
|
||||
fn local_singbox_config(selected_server_tag: &str) -> LocalSingBoxConfig {
|
||||
LocalSingBoxConfig {
|
||||
subscription_url: Some("https://sub.example.test/list".to_string()),
|
||||
selected_server_tag: Some(selected_server_tag.to_string()),
|
||||
listen_host: "127.0.0.1".to_string(),
|
||||
listen_port: 1080,
|
||||
service_name: "VpnProxySingBox".to_string(),
|
||||
install_root: r"C:\Program Files\VpnProxy\sing-box".to_string(),
|
||||
updated_at: Some("2026-07-07T10:00:00Z".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn subscription_cache() -> SubscriptionCache {
|
||||
SubscriptionCache {
|
||||
config: serde_json::json!({
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "nl-1",
|
||||
"server": "nl.example.test",
|
||||
"server_port": 443,
|
||||
"uuid": "11111111-1111-1111-1111-111111111111"
|
||||
},
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
]
|
||||
}),
|
||||
servers: vec![SubscriptionServer {
|
||||
tag: "nl-1".to_string(),
|
||||
server_type: "vless".to_string(),
|
||||
server: "nl.example.test".to_string(),
|
||||
server_port: 443,
|
||||
}],
|
||||
user_info: serde_json::Map::new(),
|
||||
fetched_at: "2026-07-07T10:00:00Z".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn discord_profile(target_id: &str) -> Profile {
|
||||
Profile {
|
||||
id: "discord".to_string(),
|
||||
@@ -229,46 +279,6 @@ fn external_socks5_target() -> Target {
|
||||
}
|
||||
}
|
||||
|
||||
fn local_singbox_target() -> Target {
|
||||
Target {
|
||||
id: "local-singbox".to_string(),
|
||||
name: "Local sing-box".to_string(),
|
||||
kind: TargetKind::Local,
|
||||
protocol: ProxyProtocol::Socks5,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 1080,
|
||||
requires_component: Some(ComponentId::Singbox),
|
||||
}
|
||||
}
|
||||
|
||||
fn running_singbox_component() -> ComponentStatus {
|
||||
ComponentStatus {
|
||||
id: ComponentId::Singbox,
|
||||
name: "Local sing-box".to_string(),
|
||||
state: ComponentState::Running,
|
||||
installed: true,
|
||||
running: true,
|
||||
version: Some("1.11.0".to_string()),
|
||||
path: Some(r"C:\Tools\VpnProxy\sing-box\sing-box.exe".to_string()),
|
||||
problems: Vec::new(),
|
||||
actions: vec!["Restart".to_string(), "Stop".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
fn stopped_singbox_component() -> ComponentStatus {
|
||||
ComponentStatus {
|
||||
id: ComponentId::Singbox,
|
||||
name: "Local sing-box".to_string(),
|
||||
state: ComponentState::Stopped,
|
||||
installed: true,
|
||||
running: false,
|
||||
version: Some("1.11.0".to_string()),
|
||||
path: Some(r"C:\Tools\VpnProxy\sing-box\sing-box.exe".to_string()),
|
||||
problems: vec!["Service is stopped".to_string()],
|
||||
actions: vec!["Start".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
fn missing_singbox_component() -> ComponentStatus {
|
||||
ComponentStatus {
|
||||
id: ComponentId::Singbox,
|
||||
|
||||
Reference in New Issue
Block a user