diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 3d918f99..4d05baff 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -12,6 +12,7 @@ + @@ -26,7 +27,9 @@ + + - \ No newline at end of file + diff --git a/v2rayN/ServiceLib.Tests/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfigV2rayServiceTests.cs new file mode 100644 index 00000000..b085e5d9 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfigV2rayServiceTests.cs @@ -0,0 +1,228 @@ +using System.Text.Json.Nodes; +using ServiceLib; +using ServiceLib.Enums; +using ServiceLib.Models; +using ServiceLib.Services.CoreConfig; +using Xunit; + +namespace ServiceLib.Tests; + +public class CoreConfigV2rayServiceTests +{ + private const string SendThrough = "198.51.100.10"; + + [Fact] + public void GenerateClientConfigContent_OnlyAppliesSendThroughToRemoteProxyOutbounds() + { + var node = CreateProxyNode("proxy-1", "198.51.100.1", 443); + var service = new CoreConfigV2rayService(CreateContext(node)); + + var result = service.GenerateClientConfigContent(); + + Assert.True(result.Success); + + var outbounds = GetOutbounds(result.Data?.ToString()); + var proxyOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue() == Global.ProxyTag); + var directOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue() == Global.DirectTag); + var blockOutbound = outbounds.Single(outbound => outbound["tag"]!.GetValue() == Global.BlockTag); + + Assert.Equal(SendThrough, proxyOutbound["sendThrough"]?.GetValue()); + Assert.Null(directOutbound["sendThrough"]); + Assert.Null(blockOutbound["sendThrough"]); + } + + [Fact] + public void GenerateClientConfigContent_OnlyAppliesSendThroughToChainExitOutbounds() + { + var exitNode = CreateProxyNode("exit", "198.51.100.2", 443); + var entryNode = CreateProxyNode("entry", "198.51.100.3", 443); + var chainNode = CreateChainNode("chain", exitNode, entryNode); + + var service = new CoreConfigV2rayService(CreateContext( + chainNode, + allProxiesMap: new Dictionary + { + [exitNode.IndexId] = exitNode, + [entryNode.IndexId] = entryNode, + })); + + var result = service.GenerateClientConfigContent(); + + Assert.True(result.Success); + + var outbounds = GetOutbounds(result.Data?.ToString()) + .Where(outbound => outbound["protocol"]?.GetValue() is not ("freedom" or "blackhole" or "dns")) + .ToList(); + + var sendThroughOutbounds = outbounds + .Where(outbound => outbound["sendThrough"]?.GetValue() == SendThrough) + .ToList(); + var chainedOutbounds = outbounds + .Where(outbound => outbound["streamSettings"]?["sockopt"]?["dialerProxy"] is not null) + .ToList(); + + Assert.Single(sendThroughOutbounds); + Assert.All(chainedOutbounds, outbound => Assert.Null(outbound["sendThrough"])); + } + + [Fact] + public void GenerateClientConfigContent_DoesNotApplySendThroughToTunRelayLoopbackOutbound() + { + var node = CreateProxyNode("proxy-1", "198.51.100.4", 443); + var config = CreateConfig(); + config.TunModeItem.EnableLegacyProtect = false; + + var service = new CoreConfigV2rayService(CreateContext( + node, + config, + isTunEnabled: true, + tunProtectSsPort: 10811, + proxyRelaySsPort: 10812)); + + var result = service.GenerateClientConfigContent(); + + Assert.True(result.Success); + + var outbounds = GetOutbounds(result.Data?.ToString()); + Assert.DoesNotContain(outbounds, outbound => outbound["sendThrough"]?.GetValue() == SendThrough); + } + + private static CoreConfigContext CreateContext( + ProfileItem node, + Config? config = null, + Dictionary? allProxiesMap = null, + bool isTunEnabled = false, + int tunProtectSsPort = 0, + int proxyRelaySsPort = 0) + { + return new CoreConfigContext + { + Node = node, + RunCoreType = ECoreType.Xray, + AppConfig = config ?? CreateConfig(), + AllProxiesMap = allProxiesMap ?? new(), + SimpleDnsItem = new SimpleDNSItem(), + IsTunEnabled = isTunEnabled, + TunProtectSsPort = tunProtectSsPort, + ProxyRelaySsPort = proxyRelaySsPort, + }; + } + + private static Config CreateConfig() + { + return new Config + { + IndexId = string.Empty, + SubIndexId = string.Empty, + CoreBasicItem = new() + { + LogEnabled = false, + Loglevel = "warning", + MuxEnabled = false, + DefAllowInsecure = false, + DefFingerprint = Global.Fingerprints.First(), + DefUserAgent = string.Empty, + SendThrough = SendThrough, + EnableFragment = false, + EnableCacheFile4Sbox = true, + }, + TunModeItem = new() + { + EnableTun = false, + AutoRoute = true, + StrictRoute = true, + Stack = string.Empty, + Mtu = 9000, + EnableIPv6Address = false, + IcmpRouting = Global.TunIcmpRoutingPolicies.First(), + EnableLegacyProtect = false, + }, + KcpItem = new(), + GrpcItem = new(), + RoutingBasicItem = new() + { + DomainStrategy = Global.DomainStrategies.First(), + DomainStrategy4Singbox = Global.DomainStrategies4Sbox.First(), + RoutingIndexId = string.Empty, + }, + GuiItem = new(), + MsgUIItem = new(), + UiItem = new() + { + CurrentLanguage = "en", + CurrentFontFamily = string.Empty, + MainColumnItem = [], + WindowSizeItem = [], + }, + ConstItem = new(), + SpeedTestItem = new(), + Mux4RayItem = new() + { + Concurrency = 8, + XudpConcurrency = 8, + XudpProxyUDP443 = "reject", + }, + Mux4SboxItem = new() + { + Protocol = string.Empty, + }, + HysteriaItem = new(), + ClashUIItem = new() + { + ConnectionsColumnItem = [], + }, + SystemProxyItem = new(), + WebDavItem = new(), + CheckUpdateItem = new(), + Fragment4RayItem = null, + Inbound = [new InItem + { + Protocol = EInboundProtocol.socks.ToString(), + LocalPort = 10808, + UdpEnabled = true, + SniffingEnabled = true, + RouteOnly = false, + }], + GlobalHotkeys = [], + CoreTypeItem = [], + SimpleDNSItem = new(), + }; + } + + private static ProfileItem CreateProxyNode(string indexId, string address, int port) + { + return new ProfileItem + { + IndexId = indexId, + Remarks = indexId, + ConfigType = EConfigType.SOCKS, + CoreType = ECoreType.Xray, + Address = address, + Port = port, + }; + } + + private static ProfileItem CreateChainNode(string indexId, params ProfileItem[] nodes) + { + var chainNode = new ProfileItem + { + IndexId = indexId, + Remarks = indexId, + ConfigType = EConfigType.ProxyChain, + CoreType = ECoreType.Xray, + }; + chainNode.SetProtocolExtra(new ProtocolExtraItem + { + ChildItems = string.Join(',', nodes.Select(node => node.IndexId)), + }); + return chainNode; + } + + private static List GetOutbounds(string? json) + { + var root = JsonNode.Parse(json ?? throw new InvalidOperationException("Config JSON is missing"))?.AsObject() + ?? throw new InvalidOperationException("Failed to parse config JSON"); + return root["outbounds"]?.AsArray().Select(node => node!.AsObject()).ToList() + ?? throw new InvalidOperationException("Config JSON does not contain outbounds"); + } +} diff --git a/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj b/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj new file mode 100644 index 00000000..4e1ed1a7 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj @@ -0,0 +1,21 @@ + + + + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + 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..81b46ac6 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,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 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..fbab6252 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1320,6 +1320,15 @@ 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) + + + 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 +1707,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..8416287e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1317,6 +1317,15 @@ 密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。 + + 本地出站地址 (SendThrough) + + + 仅对 Xray 生效,填写本机 IPv4;留空则不设置。 + + + 请填写正确的 SendThrough IPv4 地址。 + *XHTTP 模式 @@ -1695,4 +1704,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 2c544aa5..1e72de41 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; @@ -375,6 +378,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) //_coreConfig.inbounds.Clear(); + ApplyOutboundSendThrough(); var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!; configNode["inbounds"]!.AsArray().Add(new { @@ -403,4 +407,43 @@ 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 = ShouldApplySendThrough(outbound, sendThrough) ? sendThrough : null; + } + } + + private static bool ShouldApplySendThrough(Outbounds4Ray outbound, string? sendThrough) + { + if (sendThrough.IsNullOrEmpty()) + { + return false; + } + + if (outbound.protocol is "freedom" or "blackhole" or "dns" or "loopback") + { + return false; + } + + if (outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == false) + { + return false; + } + + var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address + ?? outbound.settings?.vnext?.FirstOrDefault()?.address + ?? outbound.settings?.address?.ToString() + ?? string.Empty; + + if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return !IPAddress.TryParse(outboundAddress, out var address) || !IPAddress.IsLoopback(address); + } } 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..640a4f6f 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.sln b/v2rayN/v2rayN.sln index 4e9ee76e..adf2cbf1 100644 --- a/v2rayN/v2rayN.sln +++ b/v2rayN/v2rayN.sln @@ -32,6 +32,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Action", "GitHub Act ..\.github\workflows\winget-publish.yml = ..\.github\workflows\winget-publish.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceLib.Tests", "ServiceLib.Tests\ServiceLib.Tests.csproj", "{E0B6C5C7-ED48-42EB-947A-877779E9F555}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,10 @@ Global {CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB3DE54F-3A26-AE02-1299-311132C32156}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB3DE54F-3A26-AE02-1299-311132C32156}.Release|Any CPU.Build.0 = Release|Any CPU + {E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml index a3fc391e..5a545c65 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);