ExpectedIPs

DHR60 2025-07-12 21:24:34 +08:00
parent 97bd64ee9a
commit 78bc763cee
18 changed files with 151 additions and 18 deletions

View File

@ -128,5 +128,8 @@ public class JsonUtils
/// </summary> /// </summary>
/// <param name="obj"></param> /// <param name="obj"></param>
/// <returns></returns> /// <returns></returns>
public static JsonNode? SerializeToNode(object? obj) => JsonSerializer.SerializeToNode(obj); public static JsonNode? SerializeToNode(object? obj, JsonSerializerOptions? options = null)
{
return JsonSerializer.SerializeToNode(obj, options);
}
} }

View File

@ -571,5 +571,13 @@ public class Global
{ "engage.cloudflareclient.com", new List<string> { "162.159.192.1", "2606:4700:d0::a29f:c001" } } { "engage.cloudflareclient.com", new List<string> { "162.159.192.1", "2606:4700:d0::a29f:c001" } }
}; };
public static readonly List<string> ExpectedIPs =
[
"geoip:cn",
"geoip:ir",
"geoip:ru",
""
];
#endregion const #endregion const
} }

View File

@ -269,4 +269,5 @@ public class DNSItem
public string? SingboxStrategy4Direct { get; set; } public string? SingboxStrategy4Direct { get; set; }
public string? SingboxStrategy4Proxy { get; set; } public string? SingboxStrategy4Proxy { get; set; }
public string? Hosts { get; set; } public string? Hosts { get; set; }
public string? DirectExpectedIPs { get; set; }
} }

View File

@ -212,6 +212,8 @@ public class DnsServer4Ray
public string? address { get; set; } public string? address { get; set; }
public List<string>? domains { get; set; } public List<string>? domains { get; set; }
public bool? skipFallback { get; set; } public bool? skipFallback { get; set; }
public List<string>? expectedIPs { get; set; }
public List<string>? unexpectedIPs { get; set; }
} }
public class Routing4Ray public class Routing4Ray

View File

@ -2824,7 +2824,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Enable to Override sing-box DoH Resolver 的本地化字符串。 /// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。
/// </summary> /// </summary>
public static string TbSBDoHOverride { public static string TbSBDoHOverride {
get { get {
@ -3822,6 +3822,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Validate Direct Expected IPs 的本地化字符串。
/// </summary>
public static string TbValidateDirectExpectedIPs {
get {
return ResourceManager.GetString("TbValidateDirectExpectedIPs", resourceCulture);
}
}
/// <summary>
/// 查找类似 After configuration, validates returned IPs, returning only expected IPs 的本地化字符串。
/// </summary>
public static string TbValidateDirectExpectedIPsDesc {
get {
return ResourceManager.GetString("TbValidateDirectExpectedIPsDesc", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 xray Freedom Resolution Strategy 的本地化字符串。 /// 查找类似 xray Freedom Resolution Strategy 的本地化字符串。
/// </summary> /// </summary>

View File

@ -1429,7 +1429,7 @@
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </data>
<data name="TbSBDoHOverride" xml:space="preserve"> <data name="TbSBDoHOverride" xml:space="preserve">
<value>Enable to Override sing-box DoH Resolver</value> <value>The sing-box DoH resolution server can be overwritten</value>
</data> </data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
@ -1452,4 +1452,10 @@
<data name="ThAdvancedDNSSettings" xml:space="preserve"> <data name="ThAdvancedDNSSettings" xml:space="preserve">
<value>Advanced DNS Settings</value> <value>Advanced DNS Settings</value>
</data> </data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
</data>
</root> </root>

View File

@ -1429,7 +1429,7 @@
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </data>
<data name="TbSBDoHOverride" xml:space="preserve"> <data name="TbSBDoHOverride" xml:space="preserve">
<value>Enable to Override sing-box DoH Resolver</value> <value>The sing-box DoH resolution server can be overwritten</value>
</data> </data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
@ -1452,4 +1452,10 @@
<data name="ThAdvancedDNSSettings" xml:space="preserve"> <data name="ThAdvancedDNSSettings" xml:space="preserve">
<value>Advanced DNS Settings</value> <value>Advanced DNS Settings</value>
</data> </data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
</data>
</root> </root>

View File

@ -1429,7 +1429,7 @@
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </data>
<data name="TbSBDoHOverride" xml:space="preserve"> <data name="TbSBDoHOverride" xml:space="preserve">
<value>Enable to Override sing-box DoH Resolver</value> <value>The sing-box DoH resolution server can be overwritten</value>
</data> </data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
@ -1452,4 +1452,10 @@
<data name="ThAdvancedDNSSettings" xml:space="preserve"> <data name="ThAdvancedDNSSettings" xml:space="preserve">
<value>Advanced DNS Settings</value> <value>Advanced DNS Settings</value>
</data> </data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
</data>
</root> </root>

View File

@ -1429,7 +1429,7 @@
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </data>
<data name="TbSBDoHOverride" xml:space="preserve"> <data name="TbSBDoHOverride" xml:space="preserve">
<value>Enable to Override sing-box DoH Resolver</value> <value>The sing-box DoH resolution server can be overwritten</value>
</data> </data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
@ -1452,4 +1452,10 @@
<data name="ThAdvancedDNSSettings" xml:space="preserve"> <data name="ThAdvancedDNSSettings" xml:space="preserve">
<value>Advanced DNS Settings</value> <value>Advanced DNS Settings</value>
</data> </data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
</data>
</root> </root>

View File

@ -1449,4 +1449,10 @@
<data name="ThAdvancedDNSSettings" xml:space="preserve"> <data name="ThAdvancedDNSSettings" xml:space="preserve">
<value>DNS 进阶设置</value> <value>DNS 进阶设置</value>
</data> </data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>校验直连期望 IP</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>配置后,会对返回的 IP 的进行校验,只返回期望 IP</value>
</data>
</root> </root>

View File

@ -1426,7 +1426,7 @@
<value>Add Common DNS Hosts</value> <value>Add Common DNS Hosts</value>
</data> </data>
<data name="TbSBDoHOverride" xml:space="preserve"> <data name="TbSBDoHOverride" xml:space="preserve">
<value>Enable to Override sing-box DoH Resolver</value> <value>The sing-box DoH resolution server can be overwritten</value>
</data> </data>
<data name="TbFakeIP" xml:space="preserve"> <data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value> <value>FakeIP</value>
@ -1449,4 +1449,10 @@
<data name="ThAdvancedDNSSettings" xml:space="preserve"> <data name="ThAdvancedDNSSettings" xml:space="preserve">
<value>Advanced DNS Settings</value> <value>Advanced DNS Settings</value>
</data> </data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Direct Expected IPs</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>After configuration, validates returned IPs, returning only expected IPs</value>
</data>
</root> </root>

View File

@ -1758,6 +1758,10 @@ public class CoreConfigSingboxService
{ {
rule.server = "dns_direct"; rule.server = "dns_direct";
rule.strategy = dNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : dNSItem.SingboxStrategy4Direct; rule.strategy = dNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : dNSItem.SingboxStrategy4Direct;
if (!dNSItem.DirectExpectedIPs.IsNullOrEmpty())
{
rule.rule_set = new() { dNSItem.DirectExpectedIPs };
}
} }
else if (item.OutboundTag == Global.ProxyTag) else if (item.OutboundTag == Global.ProxyTag)
{ {

View File

@ -1,6 +1,8 @@
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
@ -1282,30 +1284,37 @@ public class CoreConfigV2rayService
v2rayConfig.dns ??= new Dns4Ray(); v2rayConfig.dns ??= new Dns4Ray();
v2rayConfig.dns.servers ??= new List<object>(); v2rayConfig.dns.servers ??= new List<object>();
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
if (proxyDomainList.Count > 0) if (proxyDomainList.Count > 0)
{ {
foreach (var dnsDomain in remoteDNSAddress) foreach (var dnsDomain in remoteDNSAddress)
{ {
var dnsServer = new DnsServer4Ray() var dnsServer = new DnsServer4Ray
{ {
address = dnsDomain, address = dnsDomain,
skipFallback = true, skipFallback = true,
domains = proxyDomainList domains = proxyDomainList
}; };
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer)); v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
} }
} }
if (directDomainList.Count > 0) if (directDomainList.Count > 0)
{ {
foreach (var dnsDomain in directDNSAddress) foreach (var dnsDomain in directDNSAddress)
{ {
var dnsServer = new DnsServer4Ray() var dnsServer = new DnsServer4Ray
{ {
address = dnsDomain, address = dnsDomain,
skipFallback = true, skipFallback = true,
domains = directDomainList domains = directDomainList,
expectedIPs = dNSItem.DirectExpectedIPs.IsNullOrEmpty() ? null : new() { dNSItem.DirectExpectedIPs }
}; };
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer)); v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
} }
} }
if (proxyGeositeList.Count > 0) if (proxyGeositeList.Count > 0)
@ -1318,7 +1327,7 @@ public class CoreConfigV2rayService
skipFallback = true, skipFallback = true,
domains = proxyGeositeList domains = proxyGeositeList
}; };
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer)); v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
} }
} }
if (directGeositeList.Count > 0) if (directGeositeList.Count > 0)
@ -1329,9 +1338,10 @@ public class CoreConfigV2rayService
{ {
address = dnsDomain, address = dnsDomain,
skipFallback = true, skipFallback = true,
domains = directGeositeList domains = directGeositeList,
expectedIPs = dNSItem.DirectExpectedIPs.IsNullOrEmpty() ? null : new() { dNSItem.DirectExpectedIPs }
}; };
v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer)); v2rayConfig.dns.servers.Add(JsonUtils.SerializeToNode(dnsServer, options));
} }
} }

View File

@ -18,6 +18,7 @@ public class DNSSettingViewModel : MyReactiveObject
[Reactive] public string? SingboxStrategy4Direct { get; set; } [Reactive] public string? SingboxStrategy4Direct { get; set; }
[Reactive] public string? SingboxStrategy4Proxy { get; set; } [Reactive] public string? SingboxStrategy4Proxy { get; set; }
[Reactive] public string? Hosts { get; set; } [Reactive] public string? Hosts { get; set; }
[Reactive] public string? DirectExpectedIPs { get; set; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
//public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCmd { get; } //public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCmd { get; }
@ -61,6 +62,7 @@ public class DNSSettingViewModel : MyReactiveObject
SingboxStrategy4Direct = item.SingboxStrategy4Direct; SingboxStrategy4Direct = item.SingboxStrategy4Direct;
SingboxStrategy4Proxy = item.SingboxStrategy4Proxy; SingboxStrategy4Proxy = item.SingboxStrategy4Proxy;
Hosts = item.Hosts; Hosts = item.Hosts;
DirectExpectedIPs = item.DirectExpectedIPs;
} }
private async Task SaveSettingAsync() private async Task SaveSettingAsync()
@ -77,6 +79,7 @@ public class DNSSettingViewModel : MyReactiveObject
_config.DNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct; _config.DNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct;
_config.DNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy; _config.DNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy;
_config.DNSItem.Hosts = Hosts; _config.DNSItem.Hosts = Hosts;
_config.DNSItem.DirectExpectedIPs = DirectExpectedIPs;
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
if (_updateView != null) if (_updateView != null)
{ {

View File

@ -175,7 +175,7 @@
<Grid <Grid
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
ColumnDefinitions="Auto,Auto,*" ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,*"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,*">
<TextBlock <TextBlock
Grid.Row="0" Grid.Row="0"
@ -231,6 +231,25 @@
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" />
<ComboBox
x:Name="cmbDirectExpectedIPs"
Grid.Row="3"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="3"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPsDesc}" />
<TextBlock
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -238,7 +257,7 @@
<TextBox <TextBox
x:Name="txtHosts" x:Name="txtHosts"
Grid.Row="4" Grid.Row="5"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"

View File

@ -24,6 +24,7 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
cmbSBResolverDNS.ItemsSource = Global.DomainDirectDNSAddress.Concat(new[] { "dhcp://auto,localhost" }); cmbSBResolverDNS.ItemsSource = Global.DomainDirectDNSAddress.Concat(new[] { "dhcp://auto,localhost" });
cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress; cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress;
cmbSBFinalResolverDNS.ItemsSource = Global.DomainPureIPDNSAddress.Concat(new[] { "dhcp://auto,localhost" }); cmbSBFinalResolverDNS.ItemsSource = Global.DomainPureIPDNSAddress.Concat(new[] { "dhcp://auto,localhost" });
cmbDirectExpectedIPs.ItemsSource = Global.ExpectedIPs;
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@ -39,6 +40,7 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
}); });

View File

@ -217,6 +217,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -281,9 +282,33 @@
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbPreventDNSLeaks}" /> Text="{x:Static resx:ResUI.TbPreventDNSLeaks}" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" />
<ComboBox
x:Name="cmbDirectExpectedIPs"
Grid.Row="3"
Grid.Column="1"
Width="200"
IsEditable="True"
Margin="{StaticResource Margin8}"
Style="{StaticResource DefComboBox}" />
<TextBlock
Grid.Row="3"
Grid.Column="2"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPsDesc}" />
<TextBlock
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -291,7 +316,7 @@
Text="{x:Static resx:ResUI.TbDNSHostsConfig}" /> Text="{x:Static resx:ResUI.TbDNSHostsConfig}" />
<TextBox <TextBox
x:Name="txtHosts" x:Name="txtHosts"
Grid.Row="4" Grid.Row="5"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"

View File

@ -24,6 +24,7 @@ public partial class DNSSettingWindow
cmbSBResolverDNS.ItemsSource = Global.DomainDirectDNSAddress.Concat(new[] { "dhcp://auto,localhost" }); cmbSBResolverDNS.ItemsSource = Global.DomainDirectDNSAddress.Concat(new[] { "dhcp://auto,localhost" });
cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress; cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress;
cmbSBFinalResolverDNS.ItemsSource = Global.DomainPureIPDNSAddress.Concat(new[] { "dhcp://auto,localhost" }); cmbSBFinalResolverDNS.ItemsSource = Global.DomainPureIPDNSAddress.Concat(new[] { "dhcp://auto,localhost" });
cmbDirectExpectedIPs.ItemsSource = Global.ExpectedIPs;
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@ -39,6 +40,7 @@ public partial class DNSSettingWindow
this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
}); });