diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs
index 3907b1a7..c0d8816d 100644
--- a/v2rayN/ServiceLib/Common/Utils.cs
+++ b/v2rayN/ServiceLib/Common/Utils.cs
@@ -522,6 +522,23 @@ public class Utils
return false;
}
+ public static bool IsIpv4(string? ip)
+ {
+ if (ip.IsNullOrEmpty())
+ {
+ return false;
+ }
+
+ ip = ip.Trim();
+ if (!IPAddress.TryParse(ip, out var address))
+ {
+ return false;
+ }
+
+ return address.AddressFamily == AddressFamily.InterNetwork
+ && ip.Count(c => c == '.') == 3;
+ }
+
public static bool IsIpAddress(string? ip)
{
if (ip.IsNullOrEmpty())
diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index 167820f0..78229e6c 100644
--- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -41,6 +41,7 @@ public static class ConfigHandler
Loglevel = "warning",
MuxEnabled = false,
};
+ config.CoreBasicItem.SendThrough = config.CoreBasicItem.SendThrough?.TrimEx();
if (config.Inbound == null)
{
diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs
index 0f0e257c..fa766f0d 100644
--- a/v2rayN/ServiceLib/Models/ConfigItems.cs
+++ b/v2rayN/ServiceLib/Models/ConfigItems.cs
@@ -15,6 +15,8 @@ public class CoreBasicItem
public string DefUserAgent { get; set; }
+ public string? SendThrough { get; set; }
+
public bool EnableFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;
diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs
index 983cb4da..1bde2dc2 100644
--- a/v2rayN/ServiceLib/Models/V2rayConfig.cs
+++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs
@@ -105,6 +105,8 @@ public class Outbounds4Ray
public string protocol { get; set; }
+ public string? sendThrough { get; set; }
+
public string? targetStrategy { get; set; }
public Outboundsettings4Ray settings { get; set; }
diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
index 4498d999..13ef508a 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
+++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
@@ -222,6 +222,15 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
+ ///
+ public static string FillCorrectSendThroughIPv4 {
+ get {
+ return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Please enter the correct port format. 的本地化字符串。
///
@@ -4005,6 +4014,33 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 e.g. 192.168.1.10 的本地化字符串。
+ ///
+ public static string TbSettingsSendThroughHint {
+ get {
+ return ResourceManager.GetString("TbSettingsSendThroughHint", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Local outbound address (SendThrough) 的本地化字符串。
+ ///
+ public static string TbSettingsSendThrough {
+ get {
+ return ResourceManager.GetString("TbSettingsSendThrough", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Only applies to Xray. Fill in a local IPv4 address; leave empty to disable. 的本地化字符串。
+ ///
+ public static string TbSettingsSendThroughTip {
+ get {
+ return ResourceManager.GetString("TbSettingsSendThroughTip", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Enable Log 的本地化字符串。
///
diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx
index 569877d7..dcc3e135 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.resx
@@ -1320,6 +1320,18 @@
The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart.
+
+ Local outbound address (SendThrough)
+
+
+ e.g. 192.168.1.10
+
+
+ Only applies to Xray. Fill in a local IPv4 address; leave empty to disable.
+
+
+ Please fill in the correct IPv4 address for SendThrough.
+
*xhttp mode
@@ -1698,4 +1710,4 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Legacy TUN Protect
-
\ No newline at end of file
+
diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
index 8c628644..b1e57efe 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
@@ -1317,6 +1317,18 @@
密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。
+
+ 本地出站地址 (SendThrough)
+
+
+ 例如 192.168.1.10
+
+
+ 仅对 Xray 生效,填写本机 IPv4;留空则不设置。
+
+
+ 请填写正确的 SendThrough IPv4 地址。
+
*XHTTP 模式
@@ -1695,4 +1707,4 @@
旧版 TUN 保护
-
\ No newline at end of file
+
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
index 71e30e45..be17e63f 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
@@ -62,6 +62,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
GenDns();
GenStatistic();
+ ApplyOutboundSendThrough();
var finalRule = BuildFinalRule();
if (!string.IsNullOrEmpty(finalRule?.balancerTag))
@@ -195,6 +196,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
_coreConfig.routing.rules.Add(rule);
}
+ ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig);
@@ -255,6 +257,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
});
_coreConfig.routing.rules.Add(BuildFinalRule());
+ ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
@@ -376,6 +379,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
//_coreConfig.inbounds.Clear();
+ ApplyOutboundSendThrough();
var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!;
configNode["inbounds"]!.AsArray().Add(new
{
@@ -405,4 +409,13 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
}
#endregion public gen function
+
+ private void ApplyOutboundSendThrough()
+ {
+ var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
+ foreach (var outbound in _coreConfig.outbounds ?? [])
+ {
+ outbound.sendThrough = sendThrough.IsNullOrEmpty() ? null : sendThrough;
+ }
+ }
}
diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
index 8b677ffd..1d26f02f 100644
--- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
@@ -20,6 +20,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool defAllowInsecure { get; set; }
[Reactive] public string defFingerprint { get; set; }
[Reactive] public string defUserAgent { get; set; }
+ [Reactive] public string sendThrough { get; set; }
[Reactive] public string mux4SboxProtocol { get; set; }
[Reactive] public bool enableCacheFile4Sbox { get; set; }
[Reactive] public int? hyUpMbps { get; set; }
@@ -154,6 +155,7 @@ public class OptionSettingViewModel : MyReactiveObject
defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure;
defFingerprint = _config.CoreBasicItem.DefFingerprint;
defUserAgent = _config.CoreBasicItem.DefUserAgent;
+ sendThrough = _config.CoreBasicItem.SendThrough;
mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
hyUpMbps = _config.HysteriaItem.UpMbps;
@@ -297,6 +299,12 @@ public class OptionSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return;
}
+ var sendThroughValue = sendThrough?.TrimEx();
+ if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue))
+ {
+ NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4);
+ return;
+ }
var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics
|| DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed
|| EnableDragDropSort != _config.UiItem.EnableDragDropSort
@@ -336,6 +344,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.CoreBasicItem.DefAllowInsecure = defAllowInsecure;
_config.CoreBasicItem.DefFingerprint = defFingerprint;
_config.CoreBasicItem.DefUserAgent = defUserAgent;
+ _config.CoreBasicItem.SendThrough = sendThrough?.TrimEx();
_config.Mux4SboxItem.Protocol = mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = hyUpMbps ?? 0;
diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
index 78483180..ea4a5979 100644
--- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
@@ -325,6 +325,27 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
+
+
+
+
diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs
index 2bfe6b4c..26e55646 100644
--- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs
@@ -76,6 +76,7 @@ public partial class OptionSettingWindow : WindowBase
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.SelectedValue).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);
diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
index a3fc391e..f841c081 100644
--- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
+++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
@@ -391,6 +391,30 @@
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
+
+
+
+
diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs
index 80560607..2af77833 100644
--- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs
+++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs
@@ -73,6 +73,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);