mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-30 01:34:08 +00:00
feat: add SSH outbound protocol (sing-box, chainable)
Add SSH as a first-class protocol alongside VMess/VLESS/Anytls/Naive: - New EConfigType.SSH (sing-box only; not in XraySupportConfigType). - ProtocolExtraItem gains SshPrivateKey / SshPrivateKeyPath / SshPrivateKeyPassphrase / SshHostKey / SshHostKeyAlgorithms / SshClientVersion. - Outbound4Sbox gains user / private_key / private_key_path / private_key_passphrase / host_key / host_key_algorithms / client_version, mapping 1:1 to sing-box's ssh outbound schema. - SingboxOutboundService emits the ssh outbound and bypasses TLS for SSH nodes; multiplex is not called for SSH (sing-box rejects it). - ConfigHandler.AddSSHServer pins CoreType=sing_box, requires a username, and requires exactly one of password / inline key / key path (inline and path are mutually exclusive). - AddServerViewModel exposes SSH fields and mirrors the same validation rules at save time. - MainWindowViewModel: AddSshServerCmd routed to AddServerAsync(SSH). - WPF + Avalonia: new gridSsh on AddServerWindow with username, password, inline PEM, key path, passphrase, host key(s), host-key algorithms, and client version. Hides transport/TLS/finalmask. - MainWindow menu item 'Add [SSH]' bound on both UIs. - Resources: menuAddSshServer + Tb*Ssh* + FillSshAuth + PleaseFillUsername (resx + Designer). - Tests: SingboxSshOutboundTests covers password-only output, inline PEM line splitting, host_key CSV trimming, and SSH-in-ProxyChain detour wiring. Chaining works through the existing ProxyChain machinery (detour field), since SSH is now a regular sing-box outbound.
This commit is contained in:
parent
f4a2086dfb
commit
7d0fd3a7bb
20 changed files with 681 additions and 4 deletions
|
|
@ -130,6 +130,36 @@ internal static class CoreConfigTestFactory
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ProfileItem CreateSshNode(ECoreType coreType = ECoreType.sing_box,
|
||||||
|
string indexId = "node-ssh-1", string remarks = "demo-ssh",
|
||||||
|
string? privateKeyPem = null, string? privateKeyPath = null,
|
||||||
|
string? hostKey = null, string? hostKeyAlgorithms = null,
|
||||||
|
string password = "secret", string username = "root")
|
||||||
|
{
|
||||||
|
var node = new ProfileItem
|
||||||
|
{
|
||||||
|
IndexId = indexId,
|
||||||
|
ConfigType = EConfigType.SSH,
|
||||||
|
CoreType = coreType,
|
||||||
|
Remarks = remarks,
|
||||||
|
Address = "ssh.example.com",
|
||||||
|
Port = 22,
|
||||||
|
Username = username,
|
||||||
|
Password = password,
|
||||||
|
Network = string.Empty,
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
Subid = string.Empty,
|
||||||
|
};
|
||||||
|
node.SetProtocolExtra(node.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
SshPrivateKey = privateKeyPem,
|
||||||
|
SshPrivateKeyPath = privateKeyPath,
|
||||||
|
SshHostKey = hostKey,
|
||||||
|
SshHostKeyAlgorithms = hostKeyAlgorithms,
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
|
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
|
||||||
IEnumerable<string> childIndexIds)
|
IEnumerable<string> childIndexIds)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Common;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Models;
|
||||||
|
using ServiceLib.Services.CoreConfig;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.CoreConfig.Singbox;
|
||||||
|
|
||||||
|
public class SingboxSshOutboundTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Ssh_PasswordOnly_EmitsExpectedFields()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSshNode(password: "p@ss");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
|
||||||
|
|
||||||
|
proxy.type.Should().Be("ssh");
|
||||||
|
proxy.server.Should().Be(node.Address);
|
||||||
|
proxy.server_port.Should().Be(node.Port);
|
||||||
|
proxy.user.Should().Be(node.Username);
|
||||||
|
proxy.password.Should().Be("p@ss");
|
||||||
|
proxy.private_key.Should().BeNull();
|
||||||
|
proxy.private_key_path.Should().BeNull();
|
||||||
|
proxy.host_key.Should().BeNull();
|
||||||
|
proxy.multiplex.Should().BeNull();
|
||||||
|
proxy.tls.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ssh_InlinePrivateKey_IsSplitIntoLines()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var pem = "-----BEGIN OPENSSH PRIVATE KEY-----\r\nAAAA\nBBBB\n\n-----END OPENSSH PRIVATE KEY-----";
|
||||||
|
var node = CoreConfigTestFactory.CreateSshNode(password: string.Empty, privateKeyPem: pem);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
|
||||||
|
|
||||||
|
proxy.private_key.Should().NotBeNull();
|
||||||
|
proxy.private_key!.Should().ContainInOrder("-----BEGIN OPENSSH PRIVATE KEY-----", "AAAA", "BBBB",
|
||||||
|
"-----END OPENSSH PRIVATE KEY-----");
|
||||||
|
proxy.private_key.Should().NotContain(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ssh_HostKeyCsv_BecomesTrimmedList()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateSshNode(
|
||||||
|
hostKey: " ssh-ed25519 AAAA , ssh-rsa BBBB ",
|
||||||
|
hostKeyAlgorithms: "ssh-ed25519, rsa-sha2-256");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
|
||||||
|
|
||||||
|
proxy.host_key.Should().Equal("ssh-ed25519 AAAA", "ssh-rsa BBBB");
|
||||||
|
proxy.host_key_algorithms.Should().Equal("ssh-ed25519", "rsa-sha2-256");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ssh_InProxyChain_ReceivesDetourTag()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var sshNode = CoreConfigTestFactory.CreateSshNode(indexId: "ssh1");
|
||||||
|
var socksNode = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "s1", "exit");
|
||||||
|
var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.sing_box, "c1", "chain",
|
||||||
|
[sshNode.IndexId, socksNode.IndexId]);
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.sing_box);
|
||||||
|
context.AllProxiesMap[sshNode.IndexId] = sshNode;
|
||||||
|
context.AllProxiesMap[socksNode.IndexId] = socksNode;
|
||||||
|
context.AllProxiesMap[chain.IndexId] = chain;
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
cfg.outbounds.Should().Contain(o => o.type == "ssh");
|
||||||
|
cfg.outbounds.Where(o => o.type == "ssh")
|
||||||
|
.Should().OnlyContain(o => !string.IsNullOrEmpty(o.detour));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ public enum EConfigType
|
||||||
HTTP = 10,
|
HTTP = 10,
|
||||||
Anytls = 11,
|
Anytls = 11,
|
||||||
Naive = 12,
|
Naive = 12,
|
||||||
|
SSH = 13,
|
||||||
PolicyGroup = 101,
|
PolicyGroup = 101,
|
||||||
ProxyChain = 102,
|
ProxyChain = 102,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,8 @@ public class Global
|
||||||
{ EConfigType.TUIC, "tuic" },
|
{ EConfigType.TUIC, "tuic" },
|
||||||
{ EConfigType.WireGuard, "wireguard" },
|
{ EConfigType.WireGuard, "wireguard" },
|
||||||
{ EConfigType.Anytls, "anytls" },
|
{ EConfigType.Anytls, "anytls" },
|
||||||
{ EConfigType.Naive, "naive" }
|
{ EConfigType.Naive, "naive" },
|
||||||
|
{ EConfigType.SSH, "ssh" }
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> VmessSecurities =
|
public static readonly List<string> VmessSecurities =
|
||||||
|
|
@ -361,6 +362,7 @@ public class Global
|
||||||
EConfigType.TUIC,
|
EConfigType.TUIC,
|
||||||
EConfigType.Anytls,
|
EConfigType.Anytls,
|
||||||
EConfigType.Naive,
|
EConfigType.Naive,
|
||||||
|
EConfigType.SSH,
|
||||||
EConfigType.WireGuard,
|
EConfigType.WireGuard,
|
||||||
EConfigType.SOCKS,
|
EConfigType.SOCKS,
|
||||||
EConfigType.HTTP,
|
EConfigType.HTTP,
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,7 @@ public static class ConfigHandler
|
||||||
EConfigType.WireGuard => await AddWireguardServer(config, item),
|
EConfigType.WireGuard => await AddWireguardServer(config, item),
|
||||||
EConfigType.Anytls => await AddAnytlsServer(config, item),
|
EConfigType.Anytls => await AddAnytlsServer(config, item),
|
||||||
EConfigType.Naive => await AddNaiveServer(config, item),
|
EConfigType.Naive => await AddNaiveServer(config, item),
|
||||||
|
EConfigType.SSH => await AddSSHServer(config, item),
|
||||||
_ => -1,
|
_ => -1,
|
||||||
};
|
};
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -887,6 +888,48 @@ public static class ConfigHandler
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add or edit an SSH server
|
||||||
|
/// Validates and processes SSH-specific settings (sing-box core only)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">Current configuration</param>
|
||||||
|
/// <param name="profileItem">SSH profile to add</param>
|
||||||
|
/// <param name="toFile">Whether to save to file</param>
|
||||||
|
/// <returns>0 if successful, -1 if failed</returns>
|
||||||
|
public static async Task<int> AddSSHServer(Config config, ProfileItem profileItem, bool toFile = true)
|
||||||
|
{
|
||||||
|
profileItem.ConfigType = EConfigType.SSH;
|
||||||
|
profileItem.CoreType = ECoreType.sing_box;
|
||||||
|
|
||||||
|
profileItem.Address = profileItem.Address.TrimEx();
|
||||||
|
profileItem.Username = profileItem.Username.TrimEx();
|
||||||
|
profileItem.Password = profileItem.Password.TrimEx();
|
||||||
|
profileItem.Network = string.Empty;
|
||||||
|
profileItem.StreamSecurity = string.Empty;
|
||||||
|
profileItem.Alpn = string.Empty;
|
||||||
|
profileItem.Fingerprint = string.Empty;
|
||||||
|
|
||||||
|
if (profileItem.Username.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extra = profileItem.GetProtocolExtra();
|
||||||
|
var hasInlineKey = extra.SshPrivateKey.IsNotEmpty();
|
||||||
|
var hasKeyPath = extra.SshPrivateKeyPath.IsNotEmpty();
|
||||||
|
if (hasInlineKey && hasKeyPath)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (profileItem.Password.IsNullOrEmpty() && !hasInlineKey && !hasKeyPath)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AddServerCommon(config, profileItem, toFile);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sort the server list by the specified column
|
/// Sort the server list by the specified column
|
||||||
/// Updates the sort order in the profile extension data
|
/// Updates the sort order in the profile extension data
|
||||||
|
|
@ -1583,6 +1626,7 @@ public static class ConfigHandler
|
||||||
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
|
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
|
||||||
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
||||||
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
||||||
|
EConfigType.SSH => await AddSSHServer(config, profileItem, false),
|
||||||
_ => -1,
|
_ => -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1780,6 +1824,7 @@ public static class ConfigHandler
|
||||||
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
|
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
|
||||||
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
||||||
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
||||||
|
EConfigType.SSH => await AddSSHServer(config, profileItem, false),
|
||||||
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
|
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
|
||||||
_ => -1,
|
_ => -1,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,15 @@ public class Outbound4Sbox : BaseServer4Sbox
|
||||||
public List<string>? outbounds { get; set; }
|
public List<string>? outbounds { get; set; }
|
||||||
public bool? interrupt_exist_connections { get; set; }
|
public bool? interrupt_exist_connections { get; set; }
|
||||||
public int? tolerance { get; set; }
|
public int? tolerance { get; set; }
|
||||||
|
|
||||||
|
// ssh
|
||||||
|
public string? user { get; set; }
|
||||||
|
public List<string>? private_key { get; set; }
|
||||||
|
public string? private_key_path { get; set; }
|
||||||
|
public string? private_key_passphrase { get; set; }
|
||||||
|
public List<string>? host_key { get; set; }
|
||||||
|
public List<string>? host_key_algorithms { get; set; }
|
||||||
|
public string? client_version { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Endpoints4Sbox : BaseServer4Sbox
|
public class Endpoints4Sbox : BaseServer4Sbox
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,14 @@ public record ProtocolExtraItem
|
||||||
public int? InsecureConcurrency { get; init; }
|
public int? InsecureConcurrency { get; init; }
|
||||||
public bool? NaiveQuic { get; init; }
|
public bool? NaiveQuic { get; init; }
|
||||||
|
|
||||||
|
// ssh
|
||||||
|
public string? SshPrivateKey { get; init; }
|
||||||
|
public string? SshPrivateKeyPath { get; init; }
|
||||||
|
public string? SshPrivateKeyPassphrase { get; init; }
|
||||||
|
public string? SshHostKey { get; init; }
|
||||||
|
public string? SshHostKeyAlgorithms { get; init; }
|
||||||
|
public string? SshClientVersion { get; init; }
|
||||||
|
|
||||||
// group profile
|
// group profile
|
||||||
public string? GroupType { get; init; }
|
public string? GroupType { get; init; }
|
||||||
public string? ChildItems { get; init; }
|
public string? ChildItems { get; init; }
|
||||||
|
|
|
||||||
56
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
56
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
|
|
@ -752,7 +752,61 @@ namespace ServiceLib.Resx {
|
||||||
return ResourceManager.GetString("menuAddNaiveServer", resourceCulture);
|
return ResourceManager.GetString("menuAddNaiveServer", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string menuAddSshServer {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("menuAddSshServer", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TbSshPrivateKey {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSshPrivateKey", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TbSshPrivateKeyPath {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSshPrivateKeyPath", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TbSshPassphrase {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSshPassphrase", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TbSshHostKey {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSshHostKey", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TbSshHostKeyAlgorithms {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSshHostKeyAlgorithms", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TbSshClientVersion {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSshClientVersion", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FillSshAuth {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FillSshAuth", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string PleaseFillUsername {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("PleaseFillUsername", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Add Policy Group 的本地化字符串。
|
/// 查找类似 Add Policy Group 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1677,6 +1677,33 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||||
<data name="menuAddNaiveServer" xml:space="preserve">
|
<data name="menuAddNaiveServer" xml:space="preserve">
|
||||||
<value>Add [NaïveProxy]</value>
|
<value>Add [NaïveProxy]</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuAddSshServer" xml:space="preserve">
|
||||||
|
<value>Add [SSH]</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSshPrivateKey" xml:space="preserve">
|
||||||
|
<value>Private key (PEM)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSshPrivateKeyPath" xml:space="preserve">
|
||||||
|
<value>Private key path</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSshPassphrase" xml:space="preserve">
|
||||||
|
<value>Passphrase</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSshHostKey" xml:space="preserve">
|
||||||
|
<value>Host key(s) (comma-separated)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSshHostKeyAlgorithms" xml:space="preserve">
|
||||||
|
<value>Host key algorithms (comma-separated)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSshClientVersion" xml:space="preserve">
|
||||||
|
<value>Client version</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillSshAuth" xml:space="preserve">
|
||||||
|
<value>Provide either a password or a private key (inline or path, not both).</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseFillUsername" xml:space="preserve">
|
||||||
|
<value>Please fill in the username.</value>
|
||||||
|
</data>
|
||||||
<data name="TbInsecureConcurrency" xml:space="preserve">
|
<data name="TbInsecureConcurrency" xml:space="preserve">
|
||||||
<value>Insecure Concurrency</value>
|
<value>Insecure Concurrency</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,27 @@ public partial class CoreConfigSingboxService
|
||||||
outbound.udp_over_tcp = protocolExtra.Uot == true ? true : null;
|
outbound.udp_over_tcp = protocolExtra.Uot == true ? true : null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EConfigType.SSH:
|
||||||
|
{
|
||||||
|
outbound.user = _node.Username.NullIfEmpty();
|
||||||
|
outbound.password = _node.Password.NullIfEmpty();
|
||||||
|
|
||||||
|
var inlineKey = protocolExtra.SshPrivateKey;
|
||||||
|
if (inlineKey.IsNotEmpty())
|
||||||
|
{
|
||||||
|
outbound.private_key = inlineKey
|
||||||
|
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)
|
||||||
|
.Select(l => l.TrimEnd())
|
||||||
|
.Where(l => l.Length > 0)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
outbound.private_key_path = protocolExtra.SshPrivateKeyPath.NullIfEmpty();
|
||||||
|
outbound.private_key_passphrase = protocolExtra.SshPrivateKeyPassphrase.NullIfEmpty();
|
||||||
|
outbound.host_key = SplitCsv(protocolExtra.SshHostKey);
|
||||||
|
outbound.host_key_algorithms = SplitCsv(protocolExtra.SshHostKeyAlgorithms);
|
||||||
|
outbound.client_version = protocolExtra.SshClientVersion.NullIfEmpty();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FillOutboundTls(outbound);
|
FillOutboundTls(outbound);
|
||||||
|
|
@ -338,6 +359,20 @@ public partial class CoreConfigSingboxService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<string>? SplitCsv(string? value)
|
||||||
|
{
|
||||||
|
if (value.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var list = value!
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(s => s.Trim())
|
||||||
|
.Where(s => s.Length > 0)
|
||||||
|
.ToList();
|
||||||
|
return list.Count > 0 ? list : null;
|
||||||
|
}
|
||||||
|
|
||||||
private void FillOutboundMux(Outbound4Sbox outbound)
|
private void FillOutboundMux(Outbound4Sbox outbound)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -369,7 +404,7 @@ public partial class CoreConfigSingboxService
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_node.ConfigType is EConfigType.Shadowsocks or EConfigType.SOCKS or EConfigType.WireGuard)
|
if (_node.ConfigType is EConfigType.Shadowsocks or EConfigType.SOCKS or EConfigType.WireGuard or EConfigType.SSH)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,24 @@ public class AddServerViewModel : MyReactiveObject
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public bool NaiveQuic { get; set; }
|
public bool NaiveQuic { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string SshPrivateKey { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string SshPrivateKeyPath { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string SshPrivateKeyPassphrase { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string SshHostKey { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string SshHostKeyAlgorithms { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string SshClientVersion { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string RawHeaderType { get; set; }
|
public string RawHeaderType { get; set; }
|
||||||
|
|
||||||
|
|
@ -297,6 +315,12 @@ public class AddServerViewModel : MyReactiveObject
|
||||||
CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
|
CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
|
||||||
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
|
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
|
||||||
NaiveQuic = protocolExtra.NaiveQuic ?? false;
|
NaiveQuic = protocolExtra.NaiveQuic ?? false;
|
||||||
|
SshPrivateKey = protocolExtra.SshPrivateKey ?? string.Empty;
|
||||||
|
SshPrivateKeyPath = protocolExtra.SshPrivateKeyPath ?? string.Empty;
|
||||||
|
SshPrivateKeyPassphrase = protocolExtra.SshPrivateKeyPassphrase ?? string.Empty;
|
||||||
|
SshHostKey = protocolExtra.SshHostKey ?? string.Empty;
|
||||||
|
SshHostKeyAlgorithms = protocolExtra.SshHostKeyAlgorithms ?? string.Empty;
|
||||||
|
SshClientVersion = protocolExtra.SshClientVersion ?? string.Empty;
|
||||||
|
|
||||||
RawHeaderType = transport.RawHeaderType ?? Global.None;
|
RawHeaderType = transport.RawHeaderType ?? Global.None;
|
||||||
Host = transport.Host ?? string.Empty;
|
Host = transport.Host ?? string.Empty;
|
||||||
|
|
@ -344,7 +368,27 @@ public class AddServerViewModel : MyReactiveObject
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (SelectedSource.ConfigType is not EConfigType.SOCKS and not EConfigType.HTTP)
|
if (SelectedSource.ConfigType == EConfigType.SSH)
|
||||||
|
{
|
||||||
|
if (SelectedSource.Username.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
NoticeManager.Instance.Enqueue(ResUI.PleaseFillUsername);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var hasInlineKey = SshPrivateKey.IsNotEmpty();
|
||||||
|
var hasKeyPath = SshPrivateKeyPath.IsNotEmpty();
|
||||||
|
if (hasInlineKey && hasKeyPath)
|
||||||
|
{
|
||||||
|
NoticeManager.Instance.Enqueue(ResUI.FillSshAuth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SelectedSource.Password.IsNullOrEmpty() && !hasInlineKey && !hasKeyPath)
|
||||||
|
{
|
||||||
|
NoticeManager.Instance.Enqueue(ResUI.FillSshAuth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (SelectedSource.ConfigType is not EConfigType.SOCKS and not EConfigType.HTTP)
|
||||||
{
|
{
|
||||||
if (SelectedSource.Password.IsNullOrEmpty())
|
if (SelectedSource.Password.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
|
|
@ -396,6 +440,12 @@ public class AddServerViewModel : MyReactiveObject
|
||||||
CongestionControl = CongestionControl.NullIfEmpty(),
|
CongestionControl = CongestionControl.NullIfEmpty(),
|
||||||
InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null,
|
InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null,
|
||||||
NaiveQuic = NaiveQuic ? true : null,
|
NaiveQuic = NaiveQuic ? true : null,
|
||||||
|
SshPrivateKey = SshPrivateKey.NullIfEmpty(),
|
||||||
|
SshPrivateKeyPath = SshPrivateKeyPath.NullIfEmpty(),
|
||||||
|
SshPrivateKeyPassphrase = SshPrivateKeyPassphrase.NullIfEmpty(),
|
||||||
|
SshHostKey = SshHostKey.NullIfEmpty(),
|
||||||
|
SshHostKeyAlgorithms = SshHostKeyAlgorithms.NullIfEmpty(),
|
||||||
|
SshClientVersion = SshClientVersion.NullIfEmpty(),
|
||||||
});
|
});
|
||||||
SelectedSource.SetTransportExtra(transport);
|
SelectedSource.SetTransportExtra(transport);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ public class MainWindowViewModel : MyReactiveObject
|
||||||
public ReactiveCommand<Unit, Unit> AddWireguardServerCmd { get; }
|
public ReactiveCommand<Unit, Unit> AddWireguardServerCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> AddAnytlsServerCmd { get; }
|
public ReactiveCommand<Unit, Unit> AddAnytlsServerCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> AddNaiveServerCmd { get; }
|
public ReactiveCommand<Unit, Unit> AddNaiveServerCmd { get; }
|
||||||
|
public ReactiveCommand<Unit, Unit> AddSshServerCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; }
|
public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> AddPolicyGroupServerCmd { get; }
|
public ReactiveCommand<Unit, Unit> AddPolicyGroupServerCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> AddProxyChainServerCmd { get; }
|
public ReactiveCommand<Unit, Unit> AddProxyChainServerCmd { get; }
|
||||||
|
|
@ -124,6 +125,10 @@ public class MainWindowViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
await AddServerAsync(EConfigType.Naive);
|
await AddServerAsync(EConfigType.Naive);
|
||||||
});
|
});
|
||||||
|
AddSshServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await AddServerAsync(EConfigType.SSH);
|
||||||
|
});
|
||||||
AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
{
|
{
|
||||||
await AddServerAsync(EConfigType.Custom);
|
await AddServerAsync(EConfigType.Custom);
|
||||||
|
|
|
||||||
|
|
@ -695,6 +695,120 @@
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left" />
|
HorizontalAlignment="Left" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
x:Name="gridSsh"
|
||||||
|
Grid.Row="2"
|
||||||
|
ColumnDefinitions="300,Auto"
|
||||||
|
IsVisible="False"
|
||||||
|
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbUsername}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtUsernameSsh"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbId3}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPasswordSsh"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshPrivateKey}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPrivateKeySsh"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Height="120"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
FontFamily="Consolas,Menlo,monospace"
|
||||||
|
TextWrapping="NoWrap" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshPrivateKeyPath}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPrivateKeyPathSsh"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshPassphrase}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPassphraseSsh"
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="5"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshHostKey}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtHostKeySsh"
|
||||||
|
Grid.Row="5"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="6"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshHostKeyAlgorithms}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtHostKeyAlgoSsh"
|
||||||
|
Grid.Row="6"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="7"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshClientVersion}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtClientVersionSsh"
|
||||||
|
Grid.Row="7"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Separator
|
<Separator
|
||||||
x:Name="sepa2"
|
x:Name="sepa2"
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,15 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
||||||
|
|
||||||
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
|
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EConfigType.SSH:
|
||||||
|
gridSsh.IsVisible = true;
|
||||||
|
sepa2.IsVisible = false;
|
||||||
|
gridTransport.IsVisible = false;
|
||||||
|
gridTls.IsVisible = false;
|
||||||
|
cmbCoreType.IsEnabled = false;
|
||||||
|
gridFinalmask.IsVisible = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
|
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
|
||||||
|
|
||||||
|
|
@ -204,6 +213,17 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
||||||
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EConfigType.SSH:
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtUsernameSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtPasswordSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshPrivateKey, v => v.txtPrivateKeySsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshPrivateKeyPath, v => v.txtPrivateKeyPathSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshPrivateKeyPassphrase, v => v.txtPassphraseSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshHostKey, v => v.txtHostKeySsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshHostKeyAlgorithms, v => v.txtHostKeyAlgoSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshClientVersion, v => v.txtClientVersionSsh.Text).DisposeWith(disposables);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables);
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
<MenuItem x:Name="menuAddTuicServer" Header="{x:Static resx:ResUI.menuAddTuicServer}" />
|
<MenuItem x:Name="menuAddTuicServer" Header="{x:Static resx:ResUI.menuAddTuicServer}" />
|
||||||
<MenuItem x:Name="menuAddAnytlsServer" Header="{x:Static resx:ResUI.menuAddAnytlsServer}" />
|
<MenuItem x:Name="menuAddAnytlsServer" Header="{x:Static resx:ResUI.menuAddAnytlsServer}" />
|
||||||
<MenuItem x:Name="menuAddNaiveServer" Header="{x:Static resx:ResUI.menuAddNaiveServer}" />
|
<MenuItem x:Name="menuAddNaiveServer" Header="{x:Static resx:ResUI.menuAddNaiveServer}" />
|
||||||
|
<MenuItem x:Name="menuAddSshServer" Header="{x:Static resx:ResUI.menuAddSshServer}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem Header="{x:Static resx:ResUI.menuSubscription}">
|
<MenuItem Header="{x:Static resx:ResUI.menuSubscription}">
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
|
||||||
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
|
||||||
|
this.BindCommand(ViewModel, vm => vm.AddSshServerCmd, v => v.menuAddSshServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
|
||||||
|
|
|
||||||
|
|
@ -912,6 +912,150 @@
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left" />
|
HorizontalAlignment="Left" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
x:Name="gridSsh"
|
||||||
|
Grid.Row="2"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="300" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbUsername}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtUsernameSsh"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbId3}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPasswordSsh"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshPrivateKey}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPrivateKeySsh"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Height="120"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshPrivateKeyPath}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPrivateKeyPathSsh"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshPassphrase}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtPassphraseSsh"
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="5"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshHostKey}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtHostKeySsh"
|
||||||
|
Grid.Row="5"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="6"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshHostKeyAlgorithms}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtHostKeyAlgoSsh"
|
||||||
|
Grid.Row="6"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="7"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.TbSshClientVersion}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtClientVersionSsh"
|
||||||
|
Grid.Row="7"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="400"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
Style="{StaticResource DefTextBox}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Separator
|
<Separator
|
||||||
x:Name="sepa2"
|
x:Name="sepa2"
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,15 @@ public partial class AddServerWindow
|
||||||
|
|
||||||
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
|
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EConfigType.SSH:
|
||||||
|
gridSsh.Visibility = Visibility.Visible;
|
||||||
|
sepa2.Visibility = Visibility.Collapsed;
|
||||||
|
gridTransport.Visibility = Visibility.Collapsed;
|
||||||
|
gridTls.Visibility = Visibility.Collapsed;
|
||||||
|
cmbCoreType.IsEnabled = false;
|
||||||
|
gridFinalmask.Visibility = Visibility.Collapsed;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
|
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
|
||||||
|
|
||||||
|
|
@ -202,6 +211,17 @@ public partial class AddServerWindow
|
||||||
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EConfigType.SSH:
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtUsernameSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtPasswordSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshPrivateKey, v => v.txtPrivateKeySsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshPrivateKeyPath, v => v.txtPrivateKeyPathSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshPrivateKeyPassphrase, v => v.txtPassphraseSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshHostKey, v => v.txtHostKeySsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshHostKeyAlgorithms, v => v.txtHostKeyAlgoSsh.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SshClientVersion, v => v.txtClientVersionSsh.Text).DisposeWith(disposables);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.Text).DisposeWith(disposables);
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,10 @@
|
||||||
x:Name="menuAddNaiveServer"
|
x:Name="menuAddNaiveServer"
|
||||||
Height="{StaticResource MenuItemHeight}"
|
Height="{StaticResource MenuItemHeight}"
|
||||||
Header="{x:Static resx:ResUI.menuAddNaiveServer}" />
|
Header="{x:Static resx:ResUI.menuAddNaiveServer}" />
|
||||||
|
<MenuItem
|
||||||
|
x:Name="menuAddSshServer"
|
||||||
|
Height="{StaticResource MenuItemHeight}"
|
||||||
|
Header="{x:Static resx:ResUI.menuAddSshServer}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ public partial class MainWindow
|
||||||
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
|
||||||
|
this.BindCommand(ViewModel, vm => vm.AddSshServerCmd, v => v.menuAddSshServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue