mirror of
https://github.com/2dust/v2rayN.git
synced 2026-07-31 04:12:04 +03:00
Hysteria Realm & Gecko (#9516)
* Hysteria Realm * Core config * Fix * Fix * Add Gecko support
This commit is contained in:
61
v2rayN/ServiceLib.Tests/Fmt/HyRealmTests.cs
Normal file
61
v2rayN/ServiceLib.Tests/Fmt/HyRealmTests.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using AwesomeAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class HyRealmTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryParse_ShouldParseValidRealm()
|
||||
{
|
||||
var str = "realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478";
|
||||
var result = HyRealm.TryParse(str, out var realm);
|
||||
result.Should().BeTrue();
|
||||
realm.Should().NotBeNull();
|
||||
|
||||
realm.IsHttp.Should().BeFalse();
|
||||
realm.Token.Should().Be("public");
|
||||
realm.RendezvousHost.Should().Be("realm.hy2.io");
|
||||
realm.RendezvousPort.Should().Be(443);
|
||||
realm.RealmName.Should().Be("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||
realm.StunList.Should().HaveCount(2);
|
||||
realm.StunList.Should().Contain("example.stun:3478");
|
||||
realm.StunList.Should().Contain("example2.stun:3478");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToUri_ShouldGenerateValidUri()
|
||||
{
|
||||
var realm = new HyRealm(
|
||||
IsHttp: false,
|
||||
Token: "public",
|
||||
RendezvousHost: "realm.hy2.io",
|
||||
RendezvousPort: 443,
|
||||
RealmName: "57f9be7c-2810-4f5b-8cb9-260bc84d6c90",
|
||||
StunList: ["example.stun:3478", "example2.stun:3478"]
|
||||
);
|
||||
var uri = realm.ToUri();
|
||||
uri.Should().Contain("realm://public@realm.hy2.io");
|
||||
uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||
uri.Should().Contain("stun=example.stun:3478");
|
||||
uri.Should().Contain("stun=example2.stun:3478");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var str = "hysteria2+realm://mytoken@rendezvous.example.com/my-cabin-1f3a8c2e9b?auth=your_password&insecure=1&pinSHA256=deadbeef#remark";
|
||||
var resolved = Hysteria2Fmt.ResolveRealm(str, out var msg);
|
||||
resolved.Should().NotBeNull();
|
||||
resolved.Password.Should().Be("your_password");
|
||||
var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm);
|
||||
result.Should().BeTrue();
|
||||
realm.Should().NotBeNull();
|
||||
realm.Token.Should().Be("mytoken");
|
||||
|
||||
// To uri
|
||||
var uri = Hysteria2Fmt.ToUri(resolved);
|
||||
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
|
||||
uri.Should().EndWith("#remark");
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,10 @@ public class Global
|
||||
|
||||
public const string Hysteria2ProtocolShare = "hy2://";
|
||||
|
||||
public const string Hysteria2RealmProtocolShare = "hysteria2+realm://";
|
||||
|
||||
public const string Hysteria2HttpRealmProtocolShare = "hysteria2+realm+http://";
|
||||
|
||||
public const string NaiveHttpsProtocolShare = "naive+https://";
|
||||
|
||||
public const string NaiveQuicProtocolShare = "naive+quic://";
|
||||
@@ -677,6 +681,14 @@ public class Global
|
||||
"mcbe:bedrock.talonmc.net",
|
||||
];
|
||||
|
||||
public static readonly List<string> DefaultRealmStunList =
|
||||
[
|
||||
"turn.cloudflare.com:3478",
|
||||
"stun.nextcloud.com:3478",
|
||||
"stun.sip.us:3478",
|
||||
"global.stun.twilio.com:3478",
|
||||
];
|
||||
|
||||
public static readonly List<string> OutboundTags =
|
||||
[
|
||||
ProxyTag,
|
||||
|
||||
@@ -725,12 +725,51 @@ public static class ConfigHandler
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||
var protocolExtra = profileItem.GetProtocolExtra();
|
||||
profileItem.SetProtocolExtra(protocolExtra with
|
||||
{
|
||||
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(),
|
||||
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
|
||||
SalamanderPass = protocolExtra.SalamanderPass?.TrimEx(),
|
||||
HopInterval = protocolExtra.HopInterval?.TrimEx(),
|
||||
});
|
||||
|
||||
if (!protocolExtra.Hy2RealmUrl.IsNullOrEmpty())
|
||||
{
|
||||
var realmResult = HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm);
|
||||
if (!realmResult || realm is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (realm.StunList.Count == 0)
|
||||
{
|
||||
realm = realm with
|
||||
{
|
||||
StunList = Global.DefaultRealmStunList,
|
||||
};
|
||||
}
|
||||
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||
{
|
||||
Hy2RealmUrl = realm.ToUri(),
|
||||
});
|
||||
}
|
||||
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||
if (isGecko)
|
||||
{
|
||||
var minPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
|
||||
var maxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||
if (minPacketSize <= 0
|
||||
|| minPacketSize > maxPacketSize
|
||||
|| maxPacketSize > 2048)
|
||||
{
|
||||
minPacketSize = 512;
|
||||
maxPacketSize = 1200;
|
||||
}
|
||||
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||
{
|
||||
GeckoMinPacketSize = minPacketSize.ToString(),
|
||||
GeckoMaxPacketSize = maxPacketSize.ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
await AddServerCommon(config, profileItem, toFile);
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -72,6 +72,11 @@ public class FmtHandler
|
||||
{
|
||||
return Hysteria2Fmt.Resolve(str, out msg);
|
||||
}
|
||||
else if (str.StartsWith(Global.Hysteria2RealmProtocolShare)
|
||||
|| str.StartsWith(Global.Hysteria2HttpRealmProtocolShare))
|
||||
{
|
||||
return Hysteria2Fmt.ResolveRealm(str, out msg);
|
||||
}
|
||||
else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
|
||||
{
|
||||
return TuicFmt.Resolve(str, out msg);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
public class Hysteria2Fmt : BaseFmt
|
||||
@@ -23,15 +25,7 @@ public class Hysteria2Fmt : BaseFmt
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
ResolveUriQuery(query, ref item);
|
||||
if (item.CertSha.IsNullOrEmpty())
|
||||
{
|
||||
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
||||
}
|
||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||
{
|
||||
Ports = GetQueryDecoded(query, "mport"),
|
||||
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
|
||||
});
|
||||
ResolveHy2UriQuery(query, ref item);
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -42,8 +36,11 @@ public class Hysteria2Fmt : BaseFmt
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var url = string.Empty;
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
if (!protocolExtraItem.Hy2RealmUrl.TrimEx().IsNullOrEmpty())
|
||||
{
|
||||
return ToUriForRealm(item);
|
||||
}
|
||||
|
||||
var remark = string.Empty;
|
||||
if (item.Remarks.IsNotEmpty())
|
||||
@@ -52,27 +49,7 @@ public class Hysteria2Fmt : BaseFmt
|
||||
}
|
||||
var dicQuery = new Dictionary<string, string>();
|
||||
ToUriQueryLite(item, ref dicQuery);
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
|
||||
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("obfs", "salamander");
|
||||
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
|
||||
}
|
||||
if (!protocolExtraItem.Ports.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
|
||||
}
|
||||
if (!item.CertSha.IsNullOrEmpty())
|
||||
{
|
||||
var sha = item.CertSha;
|
||||
var idx = sha.IndexOf(',');
|
||||
if (idx > 0)
|
||||
{
|
||||
sha = sha[..idx];
|
||||
}
|
||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
||||
}
|
||||
ToHy2UriQuery(item, ref dicQuery);
|
||||
|
||||
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||
}
|
||||
@@ -94,4 +71,154 @@ public class Hysteria2Fmt : BaseFmt
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ProfileItem? ResolveRealm(string str, out string msg)
|
||||
{
|
||||
msg = ResUI.ConfigurationFormatIncorrect;
|
||||
ProfileItem item = new()
|
||||
{
|
||||
ConfigType = EConfigType.Hysteria2
|
||||
};
|
||||
var realmStr = str["hysteria2+".Length..];
|
||||
var result = HyRealm.TryParse(realmStr, out var realm);
|
||||
if (!result || realm == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var url = Utils.TryUri(str);
|
||||
if (url == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
item.Address = realm.RendezvousHost;
|
||||
item.Port = realm.RendezvousPort;
|
||||
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
ResolveUriQuery(query, ref item);
|
||||
var authPassword = GetQueryDecoded(query, "auth");
|
||||
if (authPassword.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
item.Password = authPassword;
|
||||
ResolveHy2UriQuery(query, ref item);
|
||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||
{
|
||||
Hy2RealmUrl = realm.ToUri(),
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public static string? ToUriForRealm(ProfileItem? item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
var result = HyRealm.TryParse(protocolExtraItem.Hy2RealmUrl, out var realm);
|
||||
if (!result || realm == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var remark = string.Empty;
|
||||
if (item.Remarks.IsNotEmpty())
|
||||
{
|
||||
remark = "#" + Utils.UrlEncode(item.Remarks);
|
||||
}
|
||||
var dicQuery = new Dictionary<string, string>();
|
||||
ToUriQueryLite(item, ref dicQuery);
|
||||
ToHy2UriQuery(item, ref dicQuery);
|
||||
|
||||
dicQuery.Add("auth", Utils.UrlEncode(item.Password));
|
||||
|
||||
var queryBuilder = new StringBuilder();
|
||||
queryBuilder.Append('?');
|
||||
foreach (var kv in dicQuery)
|
||||
{
|
||||
queryBuilder.Append(kv.Key);
|
||||
queryBuilder.Append('=');
|
||||
queryBuilder.Append(kv.Value);
|
||||
queryBuilder.Append('&');
|
||||
}
|
||||
foreach (var stun in realm.StunList)
|
||||
{
|
||||
queryBuilder.Append("stun=");
|
||||
queryBuilder.Append(Utils.UrlEncode(stun));
|
||||
queryBuilder.Append('&');
|
||||
}
|
||||
var query = queryBuilder.ToString().TrimEnd('&');
|
||||
|
||||
var url = $"{Utils.UrlEncode(realm.Token)}@{GetIpv6(realm.RendezvousHost)}:{realm.RendezvousPort}";
|
||||
var scheme = realm.IsHttp ? Global.Hysteria2HttpRealmProtocolShare : Global.Hysteria2RealmProtocolShare;
|
||||
return $"{scheme}{url}/{realm.RealmName}{query}{remark}";
|
||||
}
|
||||
|
||||
private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item)
|
||||
{
|
||||
if (item.CertSha.IsNullOrEmpty())
|
||||
{
|
||||
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
||||
}
|
||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||
{
|
||||
Ports = GetQueryDecoded(query, "mport"),
|
||||
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
|
||||
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
|
||||
GeckoMinPacketSize = GetQueryDecoded(query, "minPacketSize"),
|
||||
GeckoMaxPacketSize = GetQueryDecoded(query, "maxPacketSize"),
|
||||
});
|
||||
if (GetQueryDecoded(query, "obfs") == "gecko")
|
||||
{
|
||||
// Ensure the "PacketSize" parameters are present for gecko obfs.
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
if (protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty())
|
||||
{
|
||||
protocolExtraItem = protocolExtraItem with
|
||||
{
|
||||
GeckoMinPacketSize = "512",
|
||||
};
|
||||
}
|
||||
if (protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty())
|
||||
{
|
||||
protocolExtraItem = protocolExtraItem with
|
||||
{
|
||||
GeckoMaxPacketSize = "1200",
|
||||
};
|
||||
}
|
||||
item.SetProtocolExtra(protocolExtraItem);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
||||
{
|
||||
if (!item.CertSha.IsNullOrEmpty()
|
||||
&& !item.CertSha.Contains(','))
|
||||
{
|
||||
var sha = item.CertSha;
|
||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
||||
}
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("obfs", isGecko ? "gecko" : "salamander");
|
||||
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
|
||||
if (isGecko)
|
||||
{
|
||||
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
|
||||
dicQuery.Add("minPacketSize", protocolExtraItem.GeckoMinPacketSize);
|
||||
dicQuery.Add("maxPacketSize", protocolExtraItem.GeckoMaxPacketSize);
|
||||
}
|
||||
}
|
||||
if (!protocolExtraItem.Ports.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +248,15 @@ public class HyObfs4Sbox
|
||||
{
|
||||
public string? type { get; set; }
|
||||
public string? password { get; set; }
|
||||
public int? min_packet_size { get; set; }
|
||||
public int? max_packet_size { get; set; }
|
||||
}
|
||||
|
||||
public class HyRealm4Sbox
|
||||
{
|
||||
public string? server_url { get; set; }
|
||||
public string? token { get; set; }
|
||||
public string? realm_id { get; set; }
|
||||
}
|
||||
|
||||
public class Server4Sbox : BaseServer4Sbox
|
||||
@@ -334,6 +343,7 @@ public abstract class DialFields4Sbox
|
||||
public Multiplex4Sbox? multiplex { get; set; }
|
||||
public Transport4Sbox? transport { get; set; }
|
||||
public HyObfs4Sbox? obfs { get; set; }
|
||||
public HyRealm4Sbox? realm { get; set; }
|
||||
}
|
||||
|
||||
public abstract class BaseServer4Sbox : DialFields4Sbox
|
||||
|
||||
@@ -507,6 +507,10 @@ public class MaskSettings4Ray
|
||||
|
||||
public string? password { get; set; }
|
||||
|
||||
public string? url { get; set; }
|
||||
public List<string>? stunServers { get; set; }
|
||||
public string? packetSize { get; set; }
|
||||
|
||||
// fragment
|
||||
public string? packets { get; set; }
|
||||
|
||||
|
||||
106
v2rayN/ServiceLib/Models/Dto/HyRealm.cs
Normal file
106
v2rayN/ServiceLib/Models/Dto/HyRealm.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||
// realm+http://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||
// example:
|
||||
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
|
||||
public record HyRealm(
|
||||
bool IsHttp,
|
||||
string Token,
|
||||
string RendezvousHost,
|
||||
int RendezvousPort,
|
||||
string RealmName,
|
||||
List<string> StunList
|
||||
)
|
||||
{
|
||||
public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost;
|
||||
|
||||
public static bool TryParse(string? str, out HyRealm? realm)
|
||||
{
|
||||
realm = null;
|
||||
if (str == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
var isHttp = str.StartsWith("realm+http://");
|
||||
var prefix = isHttp ? "realm+http://" : "realm://";
|
||||
if (!str.StartsWith(prefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var uri = new Uri(str);
|
||||
var token = Utils.UrlDecode(uri.UserInfo);
|
||||
var rendezvousHost = uri.Host;
|
||||
var rendezvousPort = uri.IsDefaultPort ? (isHttp ? 80 : 443) : uri.Port;
|
||||
var realmName = uri.AbsolutePath.TrimStart('/');
|
||||
var stunList = new List<string>();
|
||||
var query = uri.Query;
|
||||
if (!query.IsNullOrEmpty())
|
||||
{
|
||||
if (query.StartsWith('?'))
|
||||
{
|
||||
query = query.Substring(1);
|
||||
}
|
||||
var pairs = query.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var idx = pair.IndexOf('=');
|
||||
if (idx <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var key = pair.Substring(0, idx);
|
||||
if (!key.Equals("stun", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var val = pair.Substring(idx + 1);
|
||||
stunList.Add(Utils.UrlDecode(val));
|
||||
}
|
||||
}
|
||||
realm = new HyRealm(isHttp, token, rendezvousHost, rendezvousPort, realmName, stunList);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string ToUri()
|
||||
{
|
||||
var prefix = IsHttp ? "realm+http://" : "realm://";
|
||||
var uriBuilder = new UriBuilder
|
||||
{
|
||||
Scheme = IsHttp ? "http" : "realm",
|
||||
Host = RendezvousHost,
|
||||
Port = RendezvousPort,
|
||||
Path = "/" + RealmName,
|
||||
UserName = Utils.UrlEncode(Token),
|
||||
};
|
||||
if (StunList is { Count: > 0 })
|
||||
{
|
||||
// var query = string.Join('&', StunList.Select(s => "stun=" + Utils.UrlEncode(s)));
|
||||
// NOTE: maybe we don't need to encode the stun host:port, since it should be a valid URI component already, and encoding will make it unreadable
|
||||
var query = string.Join('&', StunList.Select(s => "stun=" + s));
|
||||
uriBuilder.Query = query;
|
||||
}
|
||||
return prefix + uriBuilder.Uri.GetComponents(
|
||||
UriComponents.UserInfo | UriComponents.HostAndPort | UriComponents.PathAndQuery, UriFormat.UriEscaped);
|
||||
}
|
||||
|
||||
public string ToUriForFinalmask()
|
||||
{
|
||||
var prefix = IsHttp ? "realm+http://" : "realm://";
|
||||
var uriBuilder = new StringBuilder();
|
||||
uriBuilder.Append(prefix);
|
||||
uriBuilder.Append(Utils.UrlEncode(Token));
|
||||
uriBuilder.Append('@');
|
||||
uriBuilder.Append(RendezvousHostPort);
|
||||
uriBuilder.Append('/');
|
||||
uriBuilder.Append(Utils.UrlEncode(RealmName));
|
||||
return uriBuilder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,12 @@ public record ProtocolExtraItem
|
||||
public int? DownMbps { get; init; }
|
||||
public string? Ports { get; init; }
|
||||
public string? HopInterval { get; init; }
|
||||
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||
// example:
|
||||
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
|
||||
public string? Hy2RealmUrl { get; init; }
|
||||
public string? GeckoMinPacketSize { get; init; }
|
||||
public string? GeckoMaxPacketSize { get; init; }
|
||||
|
||||
// naiveproxy
|
||||
public int? InsecureConcurrency { get; init; }
|
||||
|
||||
236
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
236
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
@@ -232,7 +232,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment parameter format error. Please check Length (e.g. 50-100), Interval (1-100 range), and MaxSplit (0-10000). 的本地化字符串。
|
||||
/// 查找类似 Invalid range format. Use 'from-to' (e.g., 50-100). 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string FillFragmentParameterError {
|
||||
get {
|
||||
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Invalid Realm URL. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string InvalidHy2RealmUrl {
|
||||
get {
|
||||
return ResourceManager.GetString("InvalidHy2RealmUrl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Invalid address (URL) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3186,6 +3195,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbGeckoPacketSize {
|
||||
get {
|
||||
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Global Hotkey Settings 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3249,6 +3267,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Realm URL 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbHy2RealmUrl {
|
||||
get {
|
||||
return ResourceManager.GetString("TbHy2RealmUrl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Format: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port] 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbHy2RealmUrlTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbHy2RealmUrlTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 ICMP routing policy 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4095,114 +4131,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment Length 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentLength {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentLength", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment size range in bytes (e.g., 50-100). First value must be <= second. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentLengthTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentLengthTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment Interval 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentInterval {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentInterval", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be <= second. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentIntervalTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentIntervalTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Max Split 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentMaxSplit {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentMaxSplit", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Maximum number of splits (0 = unlimited, 0-10000). Only effective for Xray-core. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentMaxSplitTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentMaxSplitTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment Packets 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentPackets {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentPackets", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentPacketsTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentPacketsTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Sing-box Fragment Strategy 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentSingboxStrategy {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentSingboxStrategy", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 TLS Fragment: standard TLS record splitting. TCP Bypass: fragment TCP packets. Double: both modes combined. sing-box only. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentSingboxStrategyTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentSingboxStrategyTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fallback Delay 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentFallbackDelay {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentFallbackDelay", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fallback delay for TLS fragment. Only effective for sing-box. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentFallbackDelayTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentFallbackDelayTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Enable hardware acceleration (requires restart) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4257,6 +4185,96 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fallback Delay 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentFallbackDelay {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentFallbackDelay", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentFallbackDelayTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentFallbackDelayTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment Interval 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentInterval {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentInterval", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be <= second. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentIntervalTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentIntervalTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment Length 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentLength {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentLength", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment size range in bytes (e.g., 50-100). First value must be <= second. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentLengthTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentLengthTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Max Split 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentMaxSplit {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentMaxSplit", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentMaxSplitTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentMaxSplitTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fragment Packets 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentPackets {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentPackets", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsFragmentPacketsTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsFragmentPacketsTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Geo files source (optional) 的本地化字符串。
|
||||
/// </summary>
|
||||
|
||||
@@ -1812,4 +1812,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||
<value>Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.</value>
|
||||
</data>
|
||||
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||
<value>Realm URL</value>
|
||||
</data>
|
||||
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||
<value>Invalid Realm URL.</value>
|
||||
</data>
|
||||
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||
<value>Format: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||
</data>
|
||||
<data name="TbGeckoPacketSize" xml:space="preserve">
|
||||
<value>Gecko Packet Size (min/max)</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1030,7 +1030,7 @@
|
||||
<value>添加 [Hysteria2]</value>
|
||||
</data>
|
||||
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
||||
<value>Hysteria 最大带宽 (Up/Dw)</value>
|
||||
<value>Hysteria 最大带宽 (上传/下载)</value>
|
||||
</data>
|
||||
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
||||
<value>使用系统 hosts</value>
|
||||
@@ -1809,4 +1809,16 @@
|
||||
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||
<value>检测到不安全配置:AllowInsecure 已启用,但未提供证书。这可能会导致中间人攻击。</value>
|
||||
</data>
|
||||
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||
<value>Realm URL</value>
|
||||
</data>
|
||||
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||
<value>Realm URL 不正确。</value>
|
||||
</data>
|
||||
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||
<value>格式:realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||
</data>
|
||||
<data name="TbGeckoPacketSize" xml:space="preserve">
|
||||
<value>Gecko 包大小 (最小/最大)</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -217,11 +217,17 @@ public partial class CoreConfigSingboxService
|
||||
|
||||
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
|
||||
{
|
||||
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||
outbound.obfs = new()
|
||||
{
|
||||
type = "salamander",
|
||||
type = isGecko ? "gecko" : "salamander",
|
||||
password = protocolExtra.SalamanderPass.TrimEx(),
|
||||
};
|
||||
if (isGecko)
|
||||
{
|
||||
outbound.obfs.min_packet_size = protocolExtra.GeckoMinPacketSize.ToInt();
|
||||
outbound.obfs.max_packet_size = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||
}
|
||||
}
|
||||
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
|
||||
? su
|
||||
@@ -264,6 +270,21 @@ public partial class CoreConfigSingboxService
|
||||
}
|
||||
}
|
||||
|
||||
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
|
||||
&& realm is not null)
|
||||
{
|
||||
var realm4Sbox = new HyRealm4Sbox()
|
||||
{
|
||||
server_url = realm.RendezvousHostPort,
|
||||
token = realm.Token,
|
||||
realm_id = realm.RealmName,
|
||||
};
|
||||
outbound.realm = realm4Sbox;
|
||||
outbound.server = null;
|
||||
outbound.server_port = null;
|
||||
outbound.server_ports = null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EConfigType.TUIC:
|
||||
|
||||
@@ -607,16 +607,32 @@ public partial class CoreConfigV2rayService
|
||||
quicParams.congestion = "bbr";
|
||||
}
|
||||
hy2Finalmask.quicParams = quicParams;
|
||||
hy2Finalmask.udp ??= [];
|
||||
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
|
||||
&& realm is not null)
|
||||
{
|
||||
hy2Finalmask.udp.Add(new Mask4Ray
|
||||
{
|
||||
type = "realm",
|
||||
settings = new MaskSettings4Ray { url = realm.ToUriForFinalmask(), stunServers = realm.StunList },
|
||||
});
|
||||
}
|
||||
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
|
||||
{
|
||||
hy2Finalmask.udp =
|
||||
[
|
||||
new Mask4Ray
|
||||
{
|
||||
type = "salamander",
|
||||
settings = new MaskSettings4Ray { password = protocolExtra.SalamanderPass.TrimEx(), }
|
||||
}
|
||||
];
|
||||
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||
var salamanderSettings = new MaskSettings4Ray
|
||||
{
|
||||
password = protocolExtra.SalamanderPass.TrimEx(),
|
||||
};
|
||||
if (isGecko)
|
||||
{
|
||||
salamanderSettings.packetSize = $"{protocolExtra.GeckoMinPacketSize}-{protocolExtra.GeckoMaxPacketSize}";
|
||||
}
|
||||
hy2Finalmask.udp.Add(new Mask4Ray
|
||||
{
|
||||
type = "salamander",
|
||||
settings = salamanderSettings,
|
||||
});
|
||||
}
|
||||
streamSettings.hysteriaSettings = new()
|
||||
{
|
||||
|
||||
@@ -80,6 +80,15 @@ public class AddServerViewModel : MyReactiveObject
|
||||
[Reactive]
|
||||
public bool NaiveQuic { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string Hy2RealmUrl { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public int GeckoMinPacketSize { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public int GeckoMaxPacketSize { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string RawHeaderType { get; set; }
|
||||
|
||||
@@ -302,6 +311,9 @@ public class AddServerViewModel : MyReactiveObject
|
||||
CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
|
||||
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
|
||||
NaiveQuic = protocolExtra.NaiveQuic ?? false;
|
||||
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
|
||||
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
|
||||
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||
|
||||
RawHeaderType = transport.RawHeaderType ?? Global.None;
|
||||
Host = transport.Host ?? string.Empty;
|
||||
@@ -357,6 +369,16 @@ public class AddServerViewModel : MyReactiveObject
|
||||
return;
|
||||
}
|
||||
}
|
||||
HyRealm? realm = null;
|
||||
if (!Hy2RealmUrl.IsNullOrEmpty())
|
||||
{
|
||||
var realmResult = HyRealm.TryParse(Hy2RealmUrl, out realm);
|
||||
if (!realmResult)
|
||||
{
|
||||
NoticeManager.Instance.Enqueue(ResUI.InvalidHy2RealmUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
|
||||
SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse;
|
||||
SelectedSource.MuxEnabled = MuxEnabled;
|
||||
@@ -403,6 +425,9 @@ public class AddServerViewModel : MyReactiveObject
|
||||
CongestionControl = CongestionControl.NullIfEmpty(),
|
||||
InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null,
|
||||
NaiveQuic = NaiveQuic ? true : null,
|
||||
Hy2RealmUrl = realm?.ToUri().NullIfEmpty(),
|
||||
GeckoMinPacketSize = GeckoMinPacketSize > 0 ? GeckoMinPacketSize.ToString() : null,
|
||||
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
|
||||
});
|
||||
SelectedSource.SetTransportExtra(transport);
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="300,Auto,Auto"
|
||||
IsVisible="False"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
@@ -388,61 +388,109 @@
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource MarginLr4}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbPath7}" />
|
||||
<TextBox
|
||||
x:Name="txtPath7"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
Classes="IconButton">
|
||||
<Button.Content>
|
||||
<PathIcon Data="{StaticResource SemiIconMore}">
|
||||
<PathIcon.RenderTransform>
|
||||
<RotateTransform Angle="90" />
|
||||
</PathIcon.RenderTransform>
|
||||
</PathIcon>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<Flyout>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbPath7}" />
|
||||
<TextBox
|
||||
x:Name="txtPath7"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbGeckoPacketSize}" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox
|
||||
x:Name="txtMinGeckoPacketSize"
|
||||
Width="90"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Watermark="Min" />
|
||||
<TextBox
|
||||
x:Name="txtMaxGeckoPacketSize"
|
||||
Width="90"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Watermark="Max" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbHy2RealmUrl}" />
|
||||
<TextBox
|
||||
x:Name="txtHy2RealmUrl"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
<TextBlock
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbHy2RealmUrlTip}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbPorts7}" />
|
||||
<TextBox
|
||||
x:Name="txtPorts7"
|
||||
Grid.Row="3"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Watermark="1000-2000,3000,4000" />
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbPorts7Tips}" />
|
||||
Text="{x:Static resx:ResUI.TbPorts7Tips}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbHopInt7}" />
|
||||
<TextBox
|
||||
x:Name="txtHopInt7"
|
||||
Grid.Row="4"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsHysteriaBandwidth}" />
|
||||
<StackPanel
|
||||
Grid.Row="5"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal">
|
||||
<TextBox
|
||||
|
||||
@@ -173,6 +173,9 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
||||
this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(disposables);
|
||||
break;
|
||||
|
||||
case EConfigType.TUIC:
|
||||
|
||||
@@ -505,6 +505,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" />
|
||||
@@ -526,21 +527,62 @@
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
<materialDesign:PopupBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbPath7}" />
|
||||
<TextBox
|
||||
x:Name="txtPath7"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
StaysOpen="True"
|
||||
Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbPath7}" />
|
||||
<TextBox
|
||||
x:Name="txtPath7"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbGeckoPacketSize}" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox
|
||||
x:Name="txtMinGeckoPacketSize"
|
||||
Width="90"
|
||||
Margin="{StaticResource Margin4}"
|
||||
materialDesign:HintAssist.Hint="Min"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
<TextBox
|
||||
x:Name="txtMaxGeckoPacketSize"
|
||||
Width="90"
|
||||
Margin="{StaticResource Margin4}"
|
||||
materialDesign:HintAssist.Hint="Max"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbHy2RealmUrl}" />
|
||||
<TextBox
|
||||
x:Name="txtHy2RealmUrl"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
<TextBlock
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbHy2RealmUrlTip}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</materialDesign:PopupBox>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
@@ -561,10 +603,12 @@
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Grid.Column="2"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbPorts7Tips}" />
|
||||
Text="{x:Static resx:ResUI.TbPorts7Tips}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
|
||||
@@ -172,6 +172,9 @@ public partial class AddServerWindow
|
||||
this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(disposables);
|
||||
break;
|
||||
|
||||
case EConfigType.TUIC:
|
||||
|
||||
Reference in New Issue
Block a user