mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-05 16:22:08 +08:00
* refactor(utils): drop dead reset helper and tidy init_resources
- remove resolve_reset_async, which had no callers
- drop the duplicated time.*.com entry from the default fake-ip-filter
- init_resources no longer recreates app_home (ensure_directories during
init_config already created it) and now propagates res_dir creation errors
* refactor(profile): remove identical blind retry when switching node
The second select_node_for_group call in switch_proxy_node was a
zero-interval retry with identical arguments. Its success path was a
degraded copy of the first (missing the Refresh::Proxies announcement
ever since the old force-refresh emit was removed), and its distinct
"回退/最终失败" log wording misled maintainers. Keep a single call,
the primary success aftermath, and one error log.
* refactor(enhance): simplify tun mode check and lowercase helpers
- Drop the always-true second clause in use_tun's enhanced-mode check:
the key is absent only when the unwrap_or default already makes
current_mode == "fake-ip" true.
- Collapse use_lowercase into a delegation to use_lowercase_owned.
* refactor(core): drop unreachable hotkey dtor and dedupe tray predicate
Remove impl Drop for Hotkey: the instance lives in a OnceLock static via
the singleton! macro and is never dropped; exit-time unregistration is
already handled by Hotkey::reset().
Spell the include_proxy_groups/display-mode predicate identically at
both evaluation sites in tray menu building; behavior is unchanged
(Option != Some("disable") is equivalent to unwrap_or("default") != "disable").
* refactor(config): fold port guards into single helper
The five IClashTemp port guards duplicated the same read/parse/default
logic with drifting defaults: some hardcoded 7895/7898/7899 while others
referenced network::ports constants. Route them all through one
guard_port_value helper and use the constants everywhere, keeping each
guard's name, key and cfg attributes.
* refactor(config): dedupe prfitem factories and companion seeding
from_rules/from_proxies/from_groups differed only in uid prefix, type
string and template, so they now delegate to a shared from_seq_item.
The five current_* accessors collapse into a local macro. The identical
five-block companion seeding in from_local and from_url moves into
ensure_companion_items, preserving seeding order and error propagation.
* refactor(config): share patch macro and profiles helpers
IVerge::patch_config, IProfiles::patch_item and PrfOption::merge each
hand-rolled the same copy-Some-overwrite pattern; they now share one
patch_opt_field macro (the or-else merge form is field-for-field
equivalent for Option fields). The four profiles *_safe wrappers that
inlined Config::profiles().with_data_modify boilerplate delegate to a
with_profiles_edit helper, and record/forget_selected_node share the
current-item lookup prelude via current_item_mut.
* refactor(enhance): merge chain processing and drop unused builtin js payload
- collapse the identical process_global_items/process_profile_items into
one parameterized process_override_items (entry uid sources unchanged)
- fold the identical rules/proxies/groups arms of ChainItem::from_async
into a single arm with an inner dispatch
- stop embedding builtin JS payloads never executed by any path: the
enhance loop dispatches builtin items by uid to native implementations,
so ChainItem::builtin now carries empty script data (uids unchanged)
- source ProfileItems fallback literals from Default instead of a second
hand-written copy in collect_profile_items
* refactor(net): reuse webpki tls config and fallback
Extract build_webpki_rooted_tls_config so the delay-test TLS connector
and the reqwest clients share one static-webpki-roots builder; ALPN
stays caller-specific (only reqwest offers h2).
Collapse the duplicated platform-verifier -> static-webpki-roots retry
in get/get_bytes into request_with_static_webpki_fallback, gated by the
caller's certificate-verification setting.
* refactor(app): extract common exit cleanup
restart_app and quit duplicate the whole exit preparation: set the
exiting flag, persist configs, run interactive cleanup, and on a failed
core stop clear the flag, restore public DNS under the lifecycle lock
on macOS, and send the stop-failed notice.
Move that flow into run_exit_cleanup_flow next to clean_async, taking
the notice variant as a parameter and returning the CleanupResult so
each caller keeps its own tail: restart aborts before shutdown, quit
always shuts down and exits with the cleanup code.
* refactor(backup): share restore finalize path
restore_webdav_backup and restore_local_backup duplicated the
credential extraction (backup creation strips the WebDAV credentials,
so both re-inject the live ones), the archive extraction and the
finalize tail. Move that tail into a private restore_backup_archive
helper so the two entries keep only their download/locate logic; the
config-write lock scope and the off-runtime extraction land in one
place. The webdav entry keeps its downloaded-file cleanup.
* refactor(sysopt): drop redundant lock and busy-wait
The proxy target pair sat behind an Arc<RwLock> with no readers and an
Arc that was never cloned; both writers already serialize on
update_lock, so store the pair inside that mutex and delete the RwLock.
refresh_guard waited for the guard to become ready with a yield_now spin
at full CPU while holding guard_operation_lock; a 10 ms sleep keeps the
same wait condition and lock scope without the spin.
* refactor(sysopt): share macos bypass formatting
The macOS default bypass list and its three-way composition (empty ->
default, default + custom, custom only) were maintained twice: in
sysopt's format_bypass and inline in proxy_control's service_bypass.
Keep the literal once in constants and compose both paths through
format_bypass; service_bypass keeps its NUL check and UTF-8 truncation.
* refactor(core): dedupe window listeners, elevated runners, backup settings
- Collapse the close/focus window listener pairs in lightweight.rs onto a shared WindowListener with per-listener event and log label.
- Extract the windows runas-or-direct execution into service::run_maybe_elevated, shared by win_uwp and the service uninstaller, and factor the common uninstaller path resolution and status check shared across platforms.
- Make the auto-backup watch channel the single settings source, replacing the duplicate Arc<RwLock> copy with a stored receiver.
* refactor(enhance): fold lowercase helpers into single owned function
use_lowercase_owned disappears; the surviving use_lowercase takes the
owned Mapping and borrowed callers clone at the call site.
* refactor(enhance): rename ChainSupport variants to Mihomo names
Clash is a legacy name; the variants match the verge-mihomo and
verge-mihomo-alpha cores they gate.
* refactor(enhance): lowercase takes a reference and chain support is copy
use_lowercase borrows the mapping and clones only the values; call
sites no longer clone whole configs. ChainSupport is a fieldless enum
and now Copy; builtin() registers each support pair without clones.
* refactor(config): name prfitem constructor for what it builds
new_seq_item states that a fresh item is built from a template; the
from_ prefix wrongly implied an existing source. append_companion_item
returns the uid directly: append_item bails on a missing uid, so the
Option was always Some on the Ok path.
* refactor(sysopt): name the proxy state lock for its full role
The lock now owns the (Sysproxy, Autoproxy) state, so proxy_state_lock
says what it guards; the guard locals follow. constants::proxy held a
single sysproxy constant and is renamed sysproxy.
* refactor: replace remaining clash meta naming with mihomo
Doc comment, generated bindings and the prebuild sidecar helpers stop
referring to the legacy Clash Meta name; installer legacy-process
cleanup keeps the old executable names on purpose.
* refactor(enhance): drop builtin core filtering that never discriminates
Both valid cores matched a different ChainSupport variant, yet each
variant registered the same uid pair, so the filtered builtin list was
identical for stable and alpha. Invalid cores are coerced at startup
(validate_and_fix_config / get_valid_clash_core) and the None arm of
is_support was unreachable. builtin() now returns the items directly
and the clash_core threading through ConfigValues goes away; re-add
core discrimination when a builtin actually differs per core.
* refactor(enhance): drop unreachable mode script guard
merge_default_config overwrites the profile's mode with the verge-owned
clash config, whose mode is always present (template seeds rule and
patch semantics never clear it) and restricted to rule/global/direct;
change_clash_mode is rejected by the core before persisting anything
else. mode:script can therefore only arrive through a hand-crafted
patch_clash call, and for that path an explicit invalid-mode validation
error is more honest than silently rewriting to rule. mihomo rejects
mode:script outright (invalid mode), and user merge/script chains run
after the builtins, so the guard never covered them anyway.
* refactor(enhance): call the hysteria alpn builtin directly
builtin() existed to feed the uid-dispatch loop; with a single
builtin the vec and match are ceremony. apply_builtin_scripts calls
builtin_hy_alpn directly and to_script goes away; tests construct
ChainItem literals the way the merge items already did.
382 lines
14 KiB
Rust
382 lines
14 KiB
Rust
use crate::constants::{network, tun as tun_const};
|
|
use crate::utils::dirs::{path_to_str, sidecar_ipc_path};
|
|
use crate::utils::{dirs, help};
|
|
use anyhow::Result;
|
|
use clash_verge_logging::{Type, logging};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_yaml_ng::{Mapping, Value};
|
|
use std::{
|
|
borrow::Cow,
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
str::FromStr as _,
|
|
};
|
|
|
|
#[derive(Default, Debug, Clone)]
|
|
pub struct IClashTemp(pub Mapping);
|
|
|
|
impl IClashTemp {
|
|
pub async fn new() -> Self {
|
|
let clash_path_result = dirs::clash_path();
|
|
let map_result = if let Ok(path) = clash_path_result {
|
|
help::read_mapping(&path).await
|
|
} else {
|
|
Err(anyhow::anyhow!("Failed to get clash path"))
|
|
};
|
|
|
|
match map_result {
|
|
Ok(mut map) => {
|
|
let template_map = Self::template().0;
|
|
for (key, value) in template_map.into_iter() {
|
|
if !map.contains_key(&key) {
|
|
map.insert(key, value);
|
|
}
|
|
}
|
|
|
|
// 确保 secret 字段存在且不为空
|
|
if let Some(val) = map.get_mut("secret")
|
|
&& let Value::String(s) = val
|
|
&& s.is_empty()
|
|
{
|
|
*s = "set-your-secret".into();
|
|
}
|
|
|
|
Self(Self::guard(map))
|
|
}
|
|
Err(err) => {
|
|
logging!(
|
|
error,
|
|
Type::Config,
|
|
"failed to load clash config, using template: {err:#}"
|
|
);
|
|
Self::template()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn template() -> Self {
|
|
let mut map = Mapping::new();
|
|
let mut tun_config = Mapping::new();
|
|
let mut cors_map = Mapping::new();
|
|
|
|
tun_config.insert("enable".into(), false.into());
|
|
tun_config.insert("stack".into(), tun_const::DEFAULT_STACK.into());
|
|
tun_config.insert("auto-route".into(), true.into());
|
|
tun_config.insert("strict-route".into(), false.into());
|
|
tun_config.insert("auto-detect-interface".into(), true.into());
|
|
tun_config.insert("dns-hijack".into(), tun_const::DNS_HIJACK.into());
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
map.insert("redir-port".into(), network::ports::DEFAULT_REDIR.into());
|
|
#[cfg(target_os = "linux")]
|
|
map.insert("tproxy-port".into(), network::ports::DEFAULT_TPROXY.into());
|
|
|
|
map.insert("mixed-port".into(), network::ports::DEFAULT_MIXED.into());
|
|
map.insert("socks-port".into(), network::ports::DEFAULT_SOCKS.into());
|
|
map.insert("port".into(), network::ports::DEFAULT_HTTP.into());
|
|
map.insert("log-level".into(), "info".into());
|
|
map.insert("allow-lan".into(), false.into());
|
|
map.insert("ipv6".into(), true.into());
|
|
map.insert("mode".into(), "rule".into());
|
|
map.insert(
|
|
"external-controller".into(),
|
|
network::DEFAULT_EXTERNAL_CONTROLLER.into(),
|
|
);
|
|
#[cfg(unix)]
|
|
map.insert(
|
|
"external-controller-unix".into(),
|
|
Self::guard_external_controller_ipc().into(),
|
|
);
|
|
#[cfg(windows)]
|
|
map.insert(
|
|
"external-controller-pipe".into(),
|
|
Self::guard_external_controller_ipc().into(),
|
|
);
|
|
map.insert("tun".into(), tun_config.into());
|
|
cors_map.insert("allow-private-network".into(), true.into());
|
|
cors_map.insert(
|
|
"allow-origins".into(),
|
|
vec![
|
|
"tauri://localhost",
|
|
"http://tauri.localhost",
|
|
// Only enable this in dev mode
|
|
#[cfg(feature = "verge-dev")]
|
|
"http://localhost:3000",
|
|
"https://yacd.metacubex.one",
|
|
"https://metacubex.github.io",
|
|
"https://board.zash.run.place",
|
|
]
|
|
.into(),
|
|
);
|
|
map.insert("secret".into(), "set-your-secret".into());
|
|
map.insert("external-controller-cors".into(), cors_map.into());
|
|
map.insert("unified-delay".into(), true.into());
|
|
Self(map)
|
|
}
|
|
|
|
fn guard(mut config: Mapping) -> Mapping {
|
|
#[cfg(not(target_os = "windows"))]
|
|
let redir_port = Self::guard_redir_port(&config);
|
|
#[cfg(target_os = "linux")]
|
|
let tproxy_port = Self::guard_tproxy_port(&config);
|
|
let mixed_port = Self::guard_mixed_port(&config);
|
|
let socks_port = Self::guard_socks_port(&config);
|
|
let port = Self::guard_port(&config);
|
|
let ctrl = Self::guard_server_ctrl(&config);
|
|
#[cfg(unix)]
|
|
let external_controller_unix = Self::guard_external_controller_ipc();
|
|
#[cfg(windows)]
|
|
let external_controller_pipe = Self::guard_external_controller_ipc();
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
config.insert("redir-port".into(), redir_port.into());
|
|
#[cfg(target_os = "linux")]
|
|
config.insert("tproxy-port".into(), tproxy_port.into());
|
|
config.insert("mixed-port".into(), mixed_port.into());
|
|
config.insert("socks-port".into(), socks_port.into());
|
|
config.insert("port".into(), port.into());
|
|
config.insert("external-controller".into(), ctrl.into());
|
|
|
|
#[cfg(unix)]
|
|
config.insert("external-controller-unix".into(), external_controller_unix.into());
|
|
#[cfg(windows)]
|
|
config.insert("external-controller-pipe".into(), external_controller_pipe.into());
|
|
config
|
|
}
|
|
|
|
pub fn patch_config(&mut self, patch: &Mapping) {
|
|
for (key, value) in patch.iter() {
|
|
self.0.insert(key.to_owned(), value.to_owned());
|
|
}
|
|
}
|
|
|
|
pub async fn save_config(&self) -> Result<()> {
|
|
help::save_yaml(&dirs::clash_path()?, &self.0, Some("# Generated by Clash Verge")).await
|
|
}
|
|
|
|
pub fn get_mixed_port(&self) -> u16 {
|
|
Self::guard_mixed_port(&self.0)
|
|
}
|
|
|
|
pub fn get_client_info(&self) -> ClashInfo {
|
|
let config = &self.0;
|
|
|
|
ClashInfo {
|
|
mixed_port: Self::guard_mixed_port(config),
|
|
socks_port: Self::guard_socks_port(config),
|
|
port: Self::guard_port(config),
|
|
server: Self::guard_client_ctrl(config),
|
|
secret: config.get("secret").and_then(|value| match value {
|
|
Value::String(val_str) => Some(val_str.clone()),
|
|
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
|
Value::Number(val_num) => Some(val_num.to_string()),
|
|
_ => None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// 容错读取当前代理模式。
|
|
///
|
|
/// 仅从已保存的 clash 配置 Mapping 中提取 `mode` 字段,不依赖 mihomo `/configs`
|
|
/// 的严格 `BaseConfig` 反序列化,因此即使核心返回的字段与插件结构体不匹配也能取到。
|
|
pub fn get_mode(&self) -> Option<String> {
|
|
self.0.get("mode").and_then(|value| match value {
|
|
Value::String(val_str) => Some(val_str.clone()),
|
|
_ => None,
|
|
})
|
|
}
|
|
fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 {
|
|
let mut port = config
|
|
.get(key)
|
|
.and_then(|value| match value {
|
|
Value::String(val_str) => val_str.parse().ok(),
|
|
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
|
_ => None,
|
|
})
|
|
.unwrap_or(default);
|
|
if port == 0 {
|
|
port = default;
|
|
}
|
|
port
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn guard_redir_port(config: &Mapping) -> u16 {
|
|
Self::guard_port_value(config, "redir-port", network::ports::DEFAULT_REDIR)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn guard_tproxy_port(config: &Mapping) -> u16 {
|
|
Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY)
|
|
}
|
|
|
|
fn guard_mixed_port(config: &Mapping) -> u16 {
|
|
Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED)
|
|
}
|
|
|
|
fn guard_socks_port(config: &Mapping) -> u16 {
|
|
Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS)
|
|
}
|
|
|
|
fn guard_port(config: &Mapping) -> u16 {
|
|
Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP)
|
|
}
|
|
|
|
pub(super) fn guard_server_ctrl(config: &Mapping) -> String {
|
|
config
|
|
.get("external-controller")
|
|
.and_then(|value| match value.as_str() {
|
|
Some(val_str) => {
|
|
let val_str = val_str.trim();
|
|
|
|
let val = match val_str.starts_with(':') {
|
|
true => Cow::Owned(format!("127.0.0.1{val_str}")),
|
|
false => Cow::Borrowed(val_str),
|
|
};
|
|
|
|
SocketAddr::from_str(&val).ok().map(|s| s.to_string())
|
|
}
|
|
None => None,
|
|
})
|
|
.unwrap_or_else(|| "127.0.0.1:9097".into())
|
|
}
|
|
|
|
fn guard_client_ctrl(config: &Mapping) -> String {
|
|
let value = Self::guard_server_ctrl(config);
|
|
match SocketAddr::from_str(value.as_str()) {
|
|
Ok(mut socket) => {
|
|
if socket.ip().is_unspecified() {
|
|
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
}
|
|
socket.to_string()
|
|
}
|
|
Err(_) => "127.0.0.1:9097".into(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn guard_external_controller_ipc() -> String {
|
|
// 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致
|
|
sidecar_ipc_path()
|
|
.ok()
|
|
.and_then(|path| path_to_str(&path).ok().map(|s| s.into()))
|
|
.unwrap_or_else(|| {
|
|
logging!(error, Type::Config, "Failed to get IPC path");
|
|
crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into()
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
|
pub struct ClashInfo {
|
|
/// clash core port
|
|
pub mixed_port: u16,
|
|
pub socks_port: u16,
|
|
pub port: u16,
|
|
/// same as `external-controller`
|
|
pub server: String,
|
|
/// clash secret
|
|
pub secret: Option<String>,
|
|
}
|
|
|
|
#[test]
|
|
fn test_clash_info() {
|
|
fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {
|
|
let mut map = Mapping::new();
|
|
map.insert("mixed-port".into(), mp.into());
|
|
map.insert("external-controller".into(), ec.into());
|
|
|
|
IClashTemp(IClashTemp::guard(map)).get_client_info()
|
|
}
|
|
|
|
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
|
ClashInfo {
|
|
mixed_port: port,
|
|
socks_port: 7898,
|
|
port: 7899,
|
|
server: server.into(),
|
|
secret: None,
|
|
}
|
|
}
|
|
|
|
assert_eq!(
|
|
IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
|
|
get_result(7897, "127.0.0.1:9097")
|
|
);
|
|
|
|
assert_eq!(get_case("", ""), get_result(7897, "127.0.0.1:9097"));
|
|
|
|
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9097"));
|
|
|
|
assert_eq!(get_case(8888, "127.0.0.1:8888"), get_result(8888, "127.0.0.1:8888"));
|
|
|
|
assert_eq!(get_case(8888, " :98888 "), get_result(8888, "127.0.0.1:9097"));
|
|
|
|
assert_eq!(get_case(8888, "0.0.0.0:8080 "), get_result(8888, "127.0.0.1:8080"));
|
|
|
|
assert_eq!(get_case(8888, "0.0.0.0:8080"), get_result(8888, "127.0.0.1:8080"));
|
|
|
|
assert_eq!(get_case(8888, "[::]:8080"), get_result(8888, "127.0.0.1:8080"));
|
|
|
|
assert_eq!(get_case(8888, "192.168.1.1:8080"), get_result(8888, "192.168.1.1:8080"));
|
|
|
|
assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097"));
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct IClashExternalControllerCors {
|
|
pub allow_origins: Option<Vec<String>>,
|
|
pub allow_private_network: Option<bool>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct IClash {
|
|
pub mixed_port: Option<u16>,
|
|
pub allow_lan: Option<bool>,
|
|
pub log_level: Option<String>,
|
|
pub ipv6: Option<bool>,
|
|
pub mode: Option<String>,
|
|
pub external_controller: Option<String>,
|
|
pub secret: Option<String>,
|
|
pub dns: Option<IClashDNS>,
|
|
pub tun: Option<IClashTUN>,
|
|
pub interface_name: Option<String>,
|
|
pub external_controller_cors: Option<IClashExternalControllerCors>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct IClashTUN {
|
|
pub enable: Option<bool>,
|
|
pub stack: Option<String>,
|
|
pub auto_route: Option<bool>,
|
|
pub auto_detect_interface: Option<bool>,
|
|
pub dns_hijack: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct IClashDNS {
|
|
pub enable: Option<bool>,
|
|
pub listen: Option<String>,
|
|
pub default_nameserver: Option<Vec<String>>,
|
|
pub enhanced_mode: Option<String>,
|
|
pub fake_ip_range: Option<String>,
|
|
pub fake_ip_range6: Option<String>,
|
|
pub use_hosts: Option<bool>,
|
|
pub fake_ip_filter: Option<Vec<String>>,
|
|
pub nameserver: Option<Vec<String>>,
|
|
pub fallback: Option<Vec<String>>,
|
|
pub fallback_filter: Option<IClashFallbackFilter>,
|
|
pub nameserver_policy: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct IClashFallbackFilter {
|
|
pub geoip: Option<bool>,
|
|
pub geoip_code: Option<String>,
|
|
pub ipcidr: Option<Vec<String>>,
|
|
pub domain: Option<Vec<String>>,
|
|
}
|