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 34758618..0217d2c0 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 741c21e5..174f2341 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 6d4eabcb..f87c8c5c 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 5a1ea759..b9017dbf 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. 的本地化字符串。 /// @@ -3933,6 +3942,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 f299a877..f980da5d 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 @@ -1668,4 +1680,4 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region - \ 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 d655c30d..b64d45dd 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 模式 @@ -1665,4 +1677,4 @@ 按地区分组 - \ 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 545e713e..c4d517b5 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -59,6 +59,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) GenDns(); GenStatistic(); + ApplyOutboundSendThrough(); var finalRule = BuildFinalRule(); if (!string.IsNullOrEmpty(finalRule?.balancerTag)) @@ -192,6 +193,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); @@ -252,6 +254,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) }); _coreConfig.routing.rules.Add(BuildFinalRule()); + ApplyOutboundSendThrough(); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Success = true; @@ -347,6 +350,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) //_coreConfig.inbounds.Clear(); + ApplyOutboundSendThrough(); var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!; configNode["inbounds"]!.AsArray().Add(new { @@ -376,4 +380,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 d936c5df..873faee9 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; } @@ -152,6 +153,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; @@ -293,6 +295,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 @@ -332,6 +340,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 8c4f1fcc..4f3beac2 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 44a435af..567d41ed 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs @@ -75,6 +75,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 133b4134..5201b5b5 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 8b8647ef..de442d51 100644 --- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs @@ -72,6 +72,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);