fix(port): stop the mixed port climbing on every restart

The startup fallback persisted its replacement, so a port blocked once was never
asked for again. It now overrides the Runtime Config for the session only.

Most of those conflicts were not real: SO_REUSEADDR cannot reuse a TIME_WAIT
socket owned by another user, and a service-managed core runs as root while the
app does not. A refused bind is now confirmed by asking whether anything answers.

Also restores the borrowed port when saving proxy ports fails.
This commit is contained in:
wonfen
2026-08-17 23:30:21 +08:00
parent 906ec5e63a
commit 747a68ef2f
19 changed files with 304 additions and 120 deletions
+1 -1
View File
@@ -19,7 +19,7 @@
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增混合代理端口冲突自动回退:启动时检测端口占用并持久化可用端口,同时统一首页、设置与系统代理中的生效端口显示
- 新增混合代理端口冲突自动回退:启动时检测端口占用并临时改用可用端口,配置中的端口保持不变、下次启动重新尝试,同时统一首页、设置与系统代理中的生效端口显示
- 系统代理或 TUN 因权限被拒时弹出引导窗口,一步安装系统服务并自动恢复原本的设置
- 引导窗口显示当前阶段:安装服务 / 重启内核 / 应用代理
- 系统代理失败会保留到窗口可见时再呈现,不会因窗口隐藏或最小化而丢失
+7 -1
View File
@@ -1,10 +1,11 @@
use super::{IClashTemp, IProfiles, IVerge};
use super::{IClashTemp, IProfiles, IVerge, MixedPort};
use crate::{
config::{PrfItem, profiles_append_item_to_safe, runtime::IRuntime},
constants::{files, timing},
core::{
CoreManager,
handle::{self, Handle},
listener::MIXED_PORT_KEY,
tray,
validate::CoreConfigValidator,
},
@@ -258,6 +259,11 @@ impl Config {
let (mut config, exists_keys, logs) = enhance::enhance(profiles).await?;
sanitize_tunnels_proxy(&mut config);
// Applied here rather than in the Merge Config: this is the only place the Core's config
// is built, and `config.yaml` must stay unaware so the next launch asks for the original.
if let Some(port) = MixedPort::session_fallback() {
config.insert(MIXED_PORT_KEY.into(), port.into());
}
Self::runtime().await.edit_draft(|d| {
*d = IRuntime {
+48 -33
View File
@@ -1,16 +1,10 @@
//! Resolving the Mixed Port.
//!
//! Two different questions were being asked with one answer:
//!
//! - *What have we configured?* — the user's selection, else the Merge Config. Everything that
//! writes configuration or decides what to start needs this, and it has to be answerable
//! before the Core exists.
//! - *What is the Core actually serving on?* — what the Core reports, else what we configured.
//! Anything that points a client at the proxy wants this, because the two can differ: a
//! startup fallback moves the port, and a hand-edited Core config can move it too.
//!
//! Before, six call sites hand-copied the first chain (one of them skipping the Merge Config
//! entirely) and the frontend hand-copied a version of the second.
//! Two questions, deliberately kept apart because the answers can differ:
//! [`MixedPort::desired`] — what we configured, answerable before the Core exists;
//! [`MixedPort::effective`] — what the Core reports it is serving on.
use std::sync::atomic::{AtomicU16, Ordering};
use anyhow::Result;
@@ -20,33 +14,49 @@ use crate::core::handle::Handle;
/// The port mihomo listens on when nothing else is configured.
pub const DEFAULT_MIXED_PORT: u16 = 7897;
/// The port a startup fallback moved this session to, or 0 for none.
///
/// Never persisted: the user's port stays in `verge.yaml` and `config.yaml` so the next launch
/// asks for it again. Persisting it instead made a once-blocked port climb on every launch.
static SESSION_FALLBACK: AtomicU16 = AtomicU16::new(0);
/// Resolving the Mixed Port.
pub struct MixedPort;
impl MixedPort {
/// The Mixed Port the app has configured.
/// Read by [`Self::desired`] and by runtime config generation — the two places that decide
/// what the Core is asked to listen on.
pub(crate) fn session_fallback() -> Option<u16> {
match SESSION_FALLBACK.load(Ordering::Acquire) {
0 => None,
port => Some(port),
}
}
pub(crate) fn set_session_fallback(port: u16) {
SESSION_FALLBACK.store(port, Ordering::Release);
}
pub(crate) fn clear_session_fallback() {
SESSION_FALLBACK.store(0, Ordering::Release);
}
/// The Mixed Port the app has configured. Safe before the Core exists.
///
/// Reads the draft layer, so an edit in progress is visible to the code applying it. Safe
/// to call before the Core is running, unlike [`Self::effective`].
///
/// Reading the draft means a caller that hands this to something *outside* the app — the
/// PAC endpoint is the one that does — can name a port the Core has not moved to yet.
/// What makes that safe is that the only path staging a listener port,
/// `feat::listener::save_proxy_ports`, closes PAC across the staging with `core_starting`
/// and reopens it with `core_start_settled`. A second path that stages a listener port
/// while the Core is serving would have to do the same.
/// Reads the draft layer, so this can name a port the Core has not moved to yet — which
/// matters because the PAC endpoint hands it outside the app. Safe only because
/// `feat::listener::save_proxy_ports` closes PAC across the staging (`core_starting` /
/// `core_start_settled`); any other path staging a listener port must do the same.
pub async fn desired() -> u16 {
let selected = Config::verge().await.latest_arc().verge_mixed_port;
// `get_mixed_port` already falls back to the default when the Merge Config is silent.
let merged = Config::clash().await.latest_arc().get_mixed_port();
resolve_desired(selected, merged)
resolve_desired(Self::session_fallback(), selected, merged)
}
/// The Mixed Port the Core is actually serving on.
///
/// Costs one round-trip to the Core, so it belongs on paths a user triggers rather than on
/// paths served per request. Falls back to [`Self::desired`] when the Core cannot be asked,
/// which is the right answer whenever the Core is not running anyway.
/// Costs a round-trip, so use it on user-triggered paths, not per-request ones.
pub async fn effective() -> u16 {
let desired = Self::desired().await;
resolve_effective(
@@ -57,11 +67,12 @@ impl MixedPort {
}
}
/// Prefer what the user selected, else what the Merge Config resolved to.
const fn resolve_desired(selected: Option<u16>, merged: u16) -> u16 {
match selected {
Some(port) => port,
None => merged,
/// Fallback first — it is the only value not written down, so the files still name the port the
/// user asked for while this session serves another.
const fn resolve_desired(session_fallback: Option<u16>, selected: Option<u16>, merged: u16) -> u16 {
match (session_fallback, selected) {
(Some(port), _) | (None, Some(port)) => port,
(None, None) => merged,
}
}
@@ -85,17 +96,21 @@ mod tests {
#[test]
fn a_selected_port_wins_over_the_merge_config() {
assert_eq!(resolve_desired(Some(9000), 7897), 9000);
assert_eq!(resolve_desired(None, Some(9000), 7897), 9000);
}
#[test]
fn the_merge_config_answers_when_nothing_is_selected() {
assert_eq!(resolve_desired(None, 8080), 8080);
assert_eq!(resolve_desired(None, None, 8080), 8080);
}
#[test]
fn a_session_fallback_outranks_the_port_still_written_down() {
assert_eq!(resolve_desired(Some(7900), Some(7897), 7897), 7900);
}
#[tokio::test]
async fn a_reporting_core_overrides_what_we_configured() {
// This is the startup-fallback case: we asked for 7897 and the Core landed on 7898.
assert_eq!(resolve_effective(|| async { Ok(7898) }, 7897).await, 7898);
}
+81 -61
View File
@@ -1,12 +1,9 @@
use super::{
Config, ConfigType, IClashTemp, IVerge,
snapshot::{capture_config_files, restore_files},
};
use super::{Config, ConfigType, IClashTemp, IVerge, MixedPort};
use crate::{
constants::timing,
core::{
handle::Handle,
listener::{ListenerBindScope, MIXED_PORT_KEY, proxy_listener_keys},
listener::{ListenerBindScope, ListenerProbeOutcome, MIXED_PORT_KEY, proxy_listener_keys},
owner_identity::current_owner_credentials,
service::{SERVICE_MANAGER, ServiceStatus},
validate::CoreConfigValidator,
@@ -50,7 +47,9 @@ impl Config {
let _config_write = Self::lock_config_write().await;
let clash = Self::clash().await.latest_arc();
let verge = Self::verge().await.latest_arc();
let selected_port = clash.get_mixed_port();
// A retry must re-examine the port this session actually tried, not the one still named
// on disk, or it keeps picking the same replacement instead of walking past it.
let selected_port = MixedPort::session_fallback().unwrap_or_else(|| clash.get_mixed_port());
let bind_scope =
ListenerBindScope::from_mapping(&clash.0).context("failed to derive mixed proxy listener scope")?;
@@ -65,11 +64,24 @@ impl Config {
}
let selected_scope = bind_scope.clone();
let port_in_use = AsyncHandler::spawn_blocking(move || !selected_scope.mixed_port_is_available(selected_port))
let outcome = AsyncHandler::spawn_blocking(move || selected_scope.probe_mixed_port(selected_port))
.await
.context("mixed proxy port probe task failed")?;
if !port_in_use {
return Ok(false);
// Only a proven conflict may move the port; an answer the probe could not reach is not
// evidence. A `Conflict` here also means the probe confirmed something is serving.
match outcome {
ListenerProbeOutcome::Conflict { .. } => {}
ListenerProbeOutcome::Available => return Ok(false),
ListenerProbeOutcome::Invalid { message } | ListenerProbeOutcome::Indeterminate { message } => {
logging!(
warn,
Type::Setup,
"Could not establish whether mixed proxy port {} is free ({}); keeping it",
selected_port,
message
);
return Ok(false);
}
}
let reserved = configured_listener_ports(&clash, &verge);
@@ -86,20 +98,41 @@ impl Config {
Ok(true)
}
/// Move this session onto `new_port` without touching what is on disk.
///
/// Only the Runtime Configuration changes — the Core is its only reader and it is rebuilt
/// every launch. `config.yaml` and `verge.yaml` keep naming `old_port` so the next launch
/// asks for it again.
async fn apply_startup_mixed_port_fallback(old_port: u16, new_port: u16) -> Result<()> {
let clash = Self::clash().await;
let verge = Self::verge().await;
let runtime = Self::runtime().await;
// Roll back all three drafts if fallback fails.
let transaction = DraftTransaction::begin(vec![&clash, &verge, &runtime])?;
let transaction = DraftTransaction::begin(vec![&runtime])?;
clash.edit_draft(|draft| {
draft.0.insert(MIXED_PORT_KEY.into(), new_port.into());
});
verge.edit_draft(|draft| {
draft.verge_mixed_port = Some(new_port);
});
// On a later hop the uncommitted transaction restores the Runtime Config to the previous
// hop, so restore the override to match rather than dropping to what is on disk.
let previous_port = MixedPort::session_fallback();
MixedPort::set_session_fallback(new_port);
if let Err(error) = Self::stage_fallback_runtime().await {
match previous_port {
Some(port) => MixedPort::set_session_fallback(port),
None => MixedPort::clear_session_fallback(),
}
return Err(error);
}
transaction.commit();
record_fallback(old_port, new_port);
Handle::refresh_clash();
logging!(
warn,
Type::Config,
"Mixed proxy port {} is externally occupied; serving on {} for this session only",
old_port,
new_port
);
Ok(())
}
async fn stage_fallback_runtime() -> Result<()> {
Self::generate()
.await
.context("failed to materialize runtime configuration with fallback port")?;
@@ -112,46 +145,9 @@ impl Config {
bail!("runtime configuration with fallback port is invalid: {validation}");
}
let snapshots = capture_config_files().await?;
let candidate_clash = clash.latest_arc();
let candidate_verge = verge.latest_arc();
let persist_result = async {
candidate_clash
.save_config()
.await
.context("failed to persist Application Merge Configuration")?;
candidate_verge
.save_file()
.await
.context("failed to persist selected mixed proxy port")?;
Self::generate_file(ConfigType::Run)
.await
.context("failed to persist Runtime Configuration")?;
Ok::<(), anyhow::Error>(())
}
.await;
// Restore files after rollback so regeneration sees committed values.
if let Err(error) = persist_result {
transaction.rollback();
return match restore_files(&snapshots).await {
Ok(()) => Err(error),
Err(rollback_error) => Err(anyhow!("{error:#}; configuration rollback failed: {rollback_error:#}")),
};
}
transaction.commit();
record_fallback(old_port, new_port);
Handle::refresh_clash();
Handle::refresh_verge();
logging!(
warn,
Type::Config,
"Mixed proxy port {} is externally occupied; switched to {} and persisted all configuration layers",
old_port,
new_port
);
Self::generate_file(ConfigType::Run)
.await
.context("failed to write Runtime Configuration")?;
Ok(())
}
@@ -209,9 +205,16 @@ fn report_fallback_error(message: String) {
});
}
// Only a service-managed core can still be running during startup.
// Only a service-managed core can still be running during startup. Every exit here decides
// whether the app may renumber a port the user chose, so each one logs why.
async fn owned_service_core_uses_port(port: u16) -> bool {
if !matches!(SERVICE_MANAGER.current().await, ServiceStatus::Ready) {
let service_status = SERVICE_MANAGER.current().await;
if !matches!(service_status, ServiceStatus::Ready) {
logging!(
info,
Type::Service,
"Service is {service_status:?}, so no managed core can hold mixed proxy port {port}"
);
return false;
}
@@ -238,9 +241,26 @@ async fn owned_service_core_uses_port(port: u16) -> bool {
}
};
let Some(status) = response.data else {
logging!(
warn,
Type::Service,
"Service returned no status while checking mixed proxy port {port}"
);
return false;
};
if response.code > 0 || !status.is_active || status.core_pid.is_none() {
// Normal after a clean quit: the service drops the owner session and forgets the core, so
// it can no longer vouch for whatever holds the port. This guard only covers dirty exits.
logging!(
info,
Type::Service,
"Service no longer tracks a core for this owner (code {}, active {}, pid {:?}); \
mixed proxy port {} is unattributed",
response.code,
status.is_active,
status.core_pid,
port
);
return false;
}
+133 -9
View File
@@ -1,5 +1,4 @@
use anyhow::{Context as _, Result, anyhow, bail};
#[cfg(target_os = "macos")]
use clash_verge_logging::{Type as LogType, logging};
#[cfg(target_os = "macos")]
use network_interface::{NetworkInterface, NetworkInterfaceConfig as _};
@@ -97,7 +96,7 @@ impl ListenerBindScope {
})
}
pub(crate) fn mixed_port_is_available(&self, port: u16) -> bool {
pub(crate) fn probe_mixed_port(&self, port: u16) -> ListenerProbeOutcome {
let claims = self
.addresses
.iter()
@@ -108,7 +107,13 @@ impl ListenerBindScope {
.map(move |transport| BindClaim::new("mixed", address, port, transport))
})
.collect::<Vec<_>>();
matches!(probe_claims(&claims), ListenerProbeOutcome::Available)
probe_claims(&claims)
}
/// Whether `port` is provably free — for picking a replacement, where anything short of
/// proof should skip the candidate.
pub(crate) fn mixed_port_is_available(&self, port: u16) -> bool {
matches!(self.probe_mixed_port(port), ListenerProbeOutcome::Available)
}
}
@@ -335,12 +340,58 @@ fn claims_overlap(left: &BindClaim, right: &BindClaim) -> bool {
&& (left.address == right.address || left.address.is_unspecified() || right.address.is_unspecified())
}
/// Whether a refused bind can be dismissed because nothing is actually serving the claim.
///
/// A refused bind is not proof a port is in use: `SO_REUSEADDR` cannot reuse a `TIME_WAIT` socket
/// owned by a *different* user, and a service-managed Core runs as root while this process does
/// not. So every port the Core used is briefly unbindable here even though the Core itself could
/// rebind it at once (measured on macOS 26). "Is anything answering?" survives that asymmetry;
/// "can I bind?" does not.
///
/// Restrictions, both deliberate: a wildcard claim can be blocked by a listener on any address, so
/// one refused connection cannot clear it — leaving `allow-lan` setups exposed to the same false
/// positive. UDP has no lingering state, so a refused UDP bind is always a live socket.
fn nothing_is_serving(claim: &BindClaim) -> bool {
const ANSWER_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(200);
if claim.transport != ListenerTransport::Tcp || claim.address.is_unspecified() {
return false;
}
// Only an explicit refusal proves absence; a timeout leaves the question open.
matches!(
std::net::TcpStream::connect_timeout(&claim.socket_addr(), ANSWER_TIMEOUT),
Err(error) if error.kind() == io::ErrorKind::ConnectionRefused
)
}
fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
let mut sockets = Vec::with_capacity(claims.len());
for claim in claims {
match bind_claim(claim) {
Ok(mut bound) => sockets.append(&mut bound),
Err(error) if is_bind_conflict(&error) => return conflict_outcome(claim),
Err(error) if is_bind_conflict(&error) => {
if nothing_is_serving(claim) {
logging!(
info,
LogType::Network,
"{} could not bind {} ({}), but nothing answers there; treating it as free",
claim.name,
claim.socket_addr(),
error
);
continue;
}
logging!(
warn,
LogType::Network,
"{} cannot claim {} over {}: {}",
claim.name,
claim.socket_addr(),
transport_name(claim.transport),
error
);
return conflict_outcome(claim);
}
Err(error)
if matches!(
error.kind(),
@@ -370,7 +421,8 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
}
}
}
debug_assert!(sockets.len() >= claims.len());
// No one-socket-per-claim assertion: a dismissed claim is one this process never bound.
drop(sockets);
ListenerProbeOutcome::Available
}
@@ -425,7 +477,19 @@ fn bind_claim(claim: &BindClaim) -> io::Result<Vec<Socket>> {
match bind_socket(&BindClaim::new(claim.name, address, claim.port, claim.transport)) {
Ok(socket) => sockets.push(socket),
// Only a conflict proves the port is taken; a vanished guard address proves nothing.
Err(error) if is_bind_conflict(&error) => return Err(error),
Err(error) if is_bind_conflict(&error) => {
// The caller only sees the claim, so name the address that actually lost.
logging!(
warn,
LogType::Network,
"{} claiming {} could not hold the overlapping address {}: {}",
claim.name,
claim.socket_addr(),
SocketAddr::new(address, claim.port),
error
);
return Err(error);
}
Err(_) => {}
}
}
@@ -629,14 +693,14 @@ const fn transport_name(transport: ListenerTransport) -> &'static str {
#[cfg(test)]
mod tests {
use super::{
ListenerBindScope, ListenerProbe, ListenerProbeOutcome, ListenerTransport, probe_listener,
probe_proxy_port_change,
BindClaim, ListenerBindScope, ListenerProbe, ListenerProbeOutcome, ListenerTransport, nothing_is_serving,
probe_listener, probe_proxy_port_change,
};
use serde_json::json;
use serde_yaml_ng::Mapping;
#[cfg(unix)]
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
use std::net::{Ipv4Addr, TcpListener, UdpSocket};
use std::net::{IpAddr, Ipv4Addr, TcpListener, UdpSocket};
#[cfg(unix)]
use std::{
io::Read as _,
@@ -718,6 +782,53 @@ mod tests {
Ok(())
}
/// Guards the startup false positive: another user's `TIME_WAIT` makes the kernel refuse the
/// bind while nothing is serving. That refusal needs a second uid to stage, so this asserts
/// on the classifier directly.
#[test]
fn a_refused_bind_with_nothing_answering_is_not_a_conflict() -> anyhow::Result<()> {
let vacant = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = vacant.local_addr()?.port();
drop(vacant);
assert!(
nothing_is_serving(&BindClaim::new(
"mixed",
IpAddr::V4(Ipv4Addr::LOCALHOST),
port,
ListenerTransport::Tcp
)),
"a port nobody listens on must be dismissable"
);
let serving = TcpListener::bind((Ipv4Addr::LOCALHOST, port))?;
assert!(
!nothing_is_serving(&BindClaim::new(
"mixed",
IpAddr::V4(Ipv4Addr::LOCALHOST),
port,
ListenerTransport::Tcp
)),
"a live listener must never be dismissed"
);
drop(serving);
Ok(())
}
/// UDP has no lingering state, so a refused UDP bind is always a live socket.
#[test]
fn a_refused_udp_bind_is_never_dismissed() -> anyhow::Result<()> {
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = socket.local_addr()?.port();
assert!(!nothing_is_serving(&BindClaim::new(
"mixed",
IpAddr::V4(Ipv4Addr::LOCALHOST),
port,
ListenerTransport::Udp
)));
Ok(())
}
#[cfg(unix)]
#[test]
fn probe_reuses_port_held_only_by_a_stale_tcp_connection() -> anyhow::Result<()> {
@@ -848,6 +959,19 @@ mod tests {
Ok(())
}
/// The startup fallback only moves on a proven `Conflict`, so a taken port must report one.
#[test]
fn startup_scope_reports_a_conflict_rather_than_an_unreachable_answer() -> anyhow::Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = listener.local_addr()?.port();
let scope = ListenerBindScope::from_mapping(&mapping("ipv6: false\n")?)?;
assert!(matches!(
scope.probe_mixed_port(port),
ListenerProbeOutcome::Conflict { .. }
));
Ok(())
}
#[test]
fn startup_scope_rejects_udp_only_conflicts() -> anyhow::Result<()> {
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
+21 -2
View File
@@ -1,6 +1,6 @@
use crate::{
config::{
Config, ConfigType,
Config, ConfigType, MixedPort,
snapshot::{FileSnapshot, capture_config_files, restore_files},
},
core::{
@@ -19,7 +19,7 @@ use crate::{
use anyhow::{Context as _, Result, anyhow, bail};
use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use scopeguard::{ScopeGuard, defer, guard};
pub async fn probe_listener(request: ListenerProbe) -> Result<ListenerProbeOutcome> {
AsyncHandler::spawn_blocking(move || probe_listener_sync(&request))
@@ -55,6 +55,11 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
defer! {
manager.core_start_settled();
}
// A user-picked port ends the startup fallback, but only once the save lands: until then the
// Core still serves the borrowed port, so every failing exit must put it back or the system
// proxy and PAC point at a dead port. Declared after the PAC guard so it restores first.
let borrowed_port = guard(MixedPort::session_fallback(), restore_borrowed_port);
MixedPort::clear_session_fallback();
Config::generate()
.await
.context("failed to generate candidate proxy port configuration")?;
@@ -86,6 +91,9 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
if was_running && let Err(activation_error) = manager.restart_core_during_config_update().await {
transaction.rollback();
// Ahead of the rollback, not on scope exit: restarting the old core re-applies the system
// proxy from `MixedPort::desired()`, which must already name the borrowed port.
restore_borrowed_port(*borrowed_port);
if let Err(rollback_error) = rollback_proxy_ports(&snapshots, was_running).await {
return Err(rollback_failure(activation_error, rollback_error)
.context("failed to activate the proxy port configuration"));
@@ -105,6 +113,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
if let Err(persist_error) = persist_proxy_port_sources().await {
transaction.rollback();
restore_borrowed_port(*borrowed_port);
return match rollback_proxy_ports(&snapshots, was_running).await {
Ok(()) => Err(persist_error),
Err(rollback_error) => Err(rollback_failure(persist_error, rollback_error)),
@@ -112,12 +121,22 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
}
transaction.commit();
// The save landed, so the port the app had borrowed is now irrelevant.
let _ = ScopeGuard::into_inner(borrowed_port);
Handle::refresh_clash();
Handle::refresh_verge();
logging!(info, Type::Config, "Proxy port configuration applied and persisted");
Ok(SaveProxyPortsOutcome::Saved)
}
/// Put back the port a startup fallback had borrowed, after a save failed to replace it.
fn restore_borrowed_port(previous: Option<u16>) {
match previous {
Some(port) => MixedPort::set_session_fallback(port),
None => MixedPort::clear_session_fallback(),
}
}
async fn current_runtime_mapping() -> Result<serde_yaml_ng::Mapping> {
Config::runtime()
.await
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "المنفذ {{oldPort}} مستخدم. تم تبديل منفذ الوكيل المختلط إلى {{newPort}} وحفظه.",
"automaticFallback": "المنفذ {{oldPort}} مستخدم، لذا تعمل هذه الجلسة على {{newPort}}. لم يتغيّر إعدادك وسيُعاد تجربة {{oldPort}} عند التشغيل التالي.",
"automaticFallbackFailed": "فشل التبديل التلقائي لمنفذ الوكيل المختلط: {{error}}",
"portTooLow": "يجب ألا يقل المنفذ عن 1000",
"portTooHigh": "يجب ألا يزيد المنفذ عن 65535"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "Port {{oldPort}} ist belegt. Der gemischte Proxy-Port wurde auf {{newPort}} geändert und gespeichert.",
"automaticFallback": "Port {{oldPort}} ist belegt, daher läuft diese Sitzung auf {{newPort}}. Die Einstellung bleibt unverändert und {{oldPort}} wird beim nächsten Start erneut versucht.",
"automaticFallbackFailed": "Der automatische Wechsel des gemischten Proxy-Ports ist fehlgeschlagen: {{error}}",
"portTooLow": "Der Port muss mindestens 1000 sein",
"portTooHigh": "Der Port darf höchstens 65535 sein"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "Port {{oldPort}} was in use. The mixed proxy port was switched to {{newPort}} and saved.",
"automaticFallback": "Port {{oldPort}} was in use, so this session runs on {{newPort}}. Your setting is unchanged and {{oldPort}} is retried on the next start.",
"automaticFallbackFailed": "Automatic mixed proxy port fallback failed: {{error}}",
"portTooLow": "The port must be 1000 or higher",
"portTooHigh": "The port must be 65535 or lower"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "El puerto {{oldPort}} está en uso. El puerto proxy mixto se cambió a {{newPort}} y se guardó.",
"automaticFallback": "El puerto {{oldPort}} está en uso, por lo que esta sesión usa {{newPort}}. Tu configuración no ha cambiado y se volverá a intentar {{oldPort}} en el próximo inicio.",
"automaticFallbackFailed": "Error al cambiar automáticamente el puerto proxy mixto: {{error}}",
"portTooLow": "El puerto debe ser 1000 o superior",
"portTooHigh": "El puerto debe ser 65535 o inferior"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "پورت {{oldPort}} در حال استفاده بود. پورت پراکسی ترکیبی به {{newPort}} تغییر کرد و ذخیره شد.",
"automaticFallback": "پورت {{oldPort}} در حال استفاده بود، بنابراین این اجرا از {{newPort}} استفاده می‌کند. تنظیم شما تغییر نکرده و در اجرای بعدی {{oldPort}} دوباره امتحان می‌شود.",
"automaticFallbackFailed": "تغییر خودکار پورت پراکسی ترکیبی ناموفق بود: {{error}}",
"portTooLow": "پورت باید ۱۰۰۰ یا بیشتر باشد",
"portTooHigh": "پورت باید ۶۵۵۳۵ یا کمتر باشد"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "Port {{oldPort}} sedang digunakan. Port proxy campuran dialihkan ke {{newPort}} dan disimpan.",
"automaticFallback": "Port {{oldPort}} sedang digunakan, jadi sesi ini menggunakan {{newPort}}. Pengaturan Anda tidak berubah dan {{oldPort}} akan dicoba lagi saat mulai berikutnya.",
"automaticFallbackFailed": "Pengalihan otomatis port proxy campuran gagal: {{error}}",
"portTooLow": "Port harus 1000 atau lebih tinggi",
"portTooHigh": "Port harus 65535 atau lebih rendah"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "ポート {{oldPort}} は使用中です。混合プロキシポートを {{newPort}} に切り替えて保存しました。",
"automaticFallback": "ポート {{oldPort}} が使用中のため、今回は {{newPort}} で動作します。設定は変更されておらず、次回起動時に {{oldPort}} を再試行します。",
"automaticFallbackFailed": "混合プロキシポートの自動切り替えに失敗しました: {{error}}",
"portTooLow": "ポート番号は 1000 以上である必要があります",
"portTooHigh": "ポート番号は 65535 以下である必要があります"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "포트 설정이 저장되었습니다",
"saveFailed": "포트 설정 저장에 실패했습니다",
"automaticFallback": "포트 {{oldPort}}이(가) 사용 중입니다. 혼합 프록시 포트를 {{newPort}}(으)로 전환하고 저장했습니다.",
"automaticFallback": "포트 {{oldPort}}이(가) 사용 중이어서 이번 실행은 {{newPort}}을(를) 사용합니다. 설정은 변경되지 않았으며 다음 시작 시 {{oldPort}}을(를) 다시 시도합니다.",
"automaticFallbackFailed": "혼합 프록시 포트 자동 전환에 실패했습니다: {{error}}",
"portTooLow": "포트는 1000 이상이어야 합니다",
"portTooHigh": "포트는 65535 이하여야 합니다"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Порт {{port}} уже используется",
"saved": "Настройки портов сохранены",
"saveFailed": "Не удалось сохранить настройки портов",
"automaticFallback": "Порт {{oldPort}} занят. Смешанный прокси-порт переключён на {{newPort}} и сохранён.",
"automaticFallback": "Порт {{oldPort}} занят, поэтому этот запуск использует {{newPort}}. Настройка не изменена, при следующем запуске {{oldPort}} будет опробован снова.",
"automaticFallbackFailed": "Не удалось автоматически переключить смешанный прокси-порт: {{error}}",
"portTooLow": "Порт должен быть не меньше 1000",
"portTooHigh": "Порт должен быть не больше 65535"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "{{oldPort}} portu kullanımda. Karma proxy portu {{newPort}} olarak değiştirildi ve kaydedildi.",
"automaticFallback": "{{oldPort}} portu kullanımda olduğu için bu oturum {{newPort}} portunu kullanıyor. Ayarınız değişmedi ve sonraki başlatmada {{oldPort}} yeniden denenecek.",
"automaticFallbackFailed": "Karma proxy portu otomatik olarak değiştirilemedi: {{error}}",
"portTooLow": "Bağlantı noktası 1000 veya daha yüksek olmalıdır",
"portTooHigh": "Bağlantı noktası 65535 veya daha düşük olmalıdır"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"automaticFallback": "Port {{oldPort}} was in use. The mixed proxy port was switched to {{newPort}} and saved.",
"automaticFallback": "Port {{oldPort}} was in use, so this session runs on {{newPort}}. Your setting is unchanged and {{oldPort}} is retried on the next start.",
"automaticFallbackFailed": "Automatic mixed proxy port fallback failed: {{error}}",
"portTooLow": "Порт 1000 дән ким булмаска тиеш",
"portTooHigh": "Порт 65535 тән артмаска тиеш"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "端口 {{port}} 已被占用",
"saved": "端口设置已保存",
"saveFailed": "端口设置保存失败",
"automaticFallback": "端口 {{oldPort}} 已被占用,混合代理端口已自动切换并保存为 {{newPort}}。",
"automaticFallback": "端口 {{oldPort}} 已被占用,本次运行改用 {{newPort}}。设置未更改,下次启动会重新尝试 {{oldPort}}。",
"automaticFallbackFailed": "混合代理端口自动切换失败:{{error}}",
"portTooLow": "端口号不能小于 1000",
"portTooHigh": "端口号不能大于 65535"
+1 -1
View File
@@ -292,7 +292,7 @@
"portInUse": "Port {{port}} is already in use",
"saved": "連結埠設定已儲存",
"saveFailed": "連結埠設定儲存失敗",
"automaticFallback": "連結埠 {{oldPort}} 已被佔用,混合代理連結埠已自動切換並儲存為 {{newPort}}。",
"automaticFallback": "連結埠 {{oldPort}} 已被佔用,本次執行改用 {{newPort}}。設定未變更,下次啟動會重新嘗試 {{oldPort}}。",
"automaticFallbackFailed": "混合代理連結埠自動切換失敗:{{error}}",
"portTooLow": "連結埠不能小於 1000",
"portTooHigh": "連結埠不能大於 65535"