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);