From 0f71c46d3bd42892cc8d67918b47a369b8b13349 Mon Sep 17 00:00:00 2001 From: youugiuhiuh <260548057+youugiuhiuh@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:02:40 +0800 Subject: [PATCH] feat: support Xray XDRIVE object storage relay transport (#10546) --- .../V2ray/CoreConfigV2rayServiceTests.cs | 131 ++++++++++++++ .../ViewModels/AddServerViewModelTests.cs | 164 +++++++++++++++++ v2rayN/ServiceLib/Enums/ETransport.cs | 3 +- v2rayN/ServiceLib/Global.cs | 15 +- .../Models/CoreConfigs/V2rayConfig.cs | 23 +++ .../Models/Entities/TransportExtraItem.cs | 9 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 164 ++++++++++++----- v2rayN/ServiceLib/Resx/ResUI.az.resx | 36 ++++ v2rayN/ServiceLib/Resx/ResUI.fa.resx | 36 ++++ v2rayN/ServiceLib/Resx/ResUI.fr.resx | 36 ++++ v2rayN/ServiceLib/Resx/ResUI.hu.resx | 36 ++++ v2rayN/ServiceLib/Resx/ResUI.id.resx | 36 ++++ v2rayN/ServiceLib/Resx/ResUI.resx | 46 +++-- v2rayN/ServiceLib/Resx/ResUI.ru.resx | 36 ++++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 46 +++-- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 36 ++++ .../CoreConfig/V2ray/V2rayOutboundService.cs | 75 ++++++++ .../ViewModels/AddServerViewModel.cs | 167 +++++++++++++++++- .../Views/AddServerWindow.axaml | 119 +++++++++++++ .../Views/AddServerWindow.axaml.cs | 18 ++ 20 files changed, 1156 insertions(+), 76 deletions(-) create mode 100644 v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs index 0c290f14..06bd472b 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -824,4 +824,135 @@ public class CoreConfigV2rayServiceTests await proxyOutbound.Should().NotBeNull(); await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks"); } + + [Test] + public async Task GenerateClientConfigContent_XdriveGoogleDrive_ShouldGenerateValidOutbound() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = new ProfileItem + { + IndexId = "node-xdrive-gdrive", + ConfigType = EConfigType.VLESS, + CoreType = ECoreType.Xray, + Remarks = "xdrive-gdrive", + Address = "example.com", + Port = 443, + Password = Guid.NewGuid().ToString(), + Network = nameof(ETransport.xdrive), + MuxEnabled = false, + }; + node.SetProtocolExtra(new ProtocolExtraItem { VlessEncryption = Global.None }); + node.SetTransportExtra(new TransportExtraItem + { + XdriveService = "google-drive", + XdriveRemoteFolder = "folder-123", + XdriveSecrets = "id,secret,token", + }); + + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + await result.Success.Should().BeTrue(); + var generatedConfig = JsonNode.Parse(result.Data!.ToString()!)!; + var proxy = generatedConfig["outbounds"]!.AsArray() + .First(outbound => outbound!["tag"]!.GetValue() == Global.ProxyTag)!; + + var streamSettings = proxy["streamSettings"]!; + await streamSettings["network"]!.GetValue().Should().BeEqualTo("xdrive"); + + var xdriveSettings = streamSettings["xdriveSettings"]!; + await xdriveSettings["service"]!.GetValue().Should().BeEqualTo("google-drive"); + await xdriveSettings["remoteFolder"]!.GetValue().Should().BeEqualTo("folder-123"); + + var secrets = xdriveSettings["secrets"]!.AsArray(); + await secrets.Count.Should().BeEqualTo(3); + await secrets[0]!.GetValue().Should().BeEqualTo("id"); + await secrets[1]!.GetValue().Should().BeEqualTo("secret"); + await secrets[2]!.GetValue().Should().BeEqualTo("token"); + + await proxy["mux"]!["enabled"]!.GetValue().Should().BeTrue(); + } + + [Test] + public async Task GenerateClientConfigContent_XdriveS3_ShouldGenerateValidOutbound() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = new ProfileItem + { + IndexId = "node-xdrive-s3", + ConfigType = EConfigType.VLESS, + CoreType = ECoreType.Xray, + Remarks = "xdrive-s3", + Address = "example.com", + Port = 443, + Password = Guid.NewGuid().ToString(), + Network = nameof(ETransport.xdrive), + }; + node.SetProtocolExtra(new ProtocolExtraItem { VlessEncryption = Global.None }); + node.SetTransportExtra(new TransportExtraItem + { + XdriveService = "s3", + XdriveS3Endpoint = "https://s3.amazonaws.com", + XdriveS3Bucket = "mybucket", + XdriveS3PathStyle = false, + }); + + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + await result.Success.Should().BeTrue(); + var generatedConfig = JsonNode.Parse(result.Data!.ToString()!)!; + var proxy = generatedConfig["outbounds"]!.AsArray() + .First(outbound => outbound!["tag"]!.GetValue() == Global.ProxyTag)!; + + var s3 = proxy["streamSettings"]!["xdriveSettings"]!["s3"]!; + await s3["endpoint"]!.GetValue().Should().BeEqualTo("https://s3.amazonaws.com"); + await s3["bucket"]!.GetValue().Should().BeEqualTo("mybucket"); + await s3["pathStyle"]!.GetValue().Should().BeFalse(); + } + + [Test] + public async Task GenerateClientConfigContent_XdriveExtra_ShouldPopulateParameters() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = new ProfileItem + { + IndexId = "node-xdrive-extra", + ConfigType = EConfigType.VLESS, + CoreType = ECoreType.Xray, + Remarks = "xdrive-extra", + Address = "example.com", + Port = 443, + Password = Guid.NewGuid().ToString(), + Network = nameof(ETransport.xdrive), + }; + node.SetProtocolExtra(new ProtocolExtraItem { VlessEncryption = Global.None }); + node.SetTransportExtra(new TransportExtraItem + { + XdriveService = "onedrive", + XdriveExtra = """{"segmentBytes":1048576,"flushIntervalMs":500,"pollIntervalMs":200,"maxPollIntervalMs":2000,"eagerWindowMs":1000,"concurrency":4}""", + }); + + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + await result.Success.Should().BeTrue(); + var generatedConfig = JsonNode.Parse(result.Data!.ToString()!)!; + var proxy = generatedConfig["outbounds"]!.AsArray() + .First(outbound => outbound!["tag"]!.GetValue() == Global.ProxyTag)!; + + var xdriveSettings = proxy["streamSettings"]!["xdriveSettings"]!; + await xdriveSettings["segmentBytes"]!.GetValue().Should().BeEqualTo(1048576); + await xdriveSettings["flushIntervalMs"]!.GetValue().Should().BeEqualTo(500); + await xdriveSettings["pollIntervalMs"]!.GetValue().Should().BeEqualTo(200); + await xdriveSettings["maxPollIntervalMs"]!.GetValue().Should().BeEqualTo(2000); + await xdriveSettings["eagerWindowMs"]!.GetValue().Should().BeEqualTo(1000); + await xdriveSettings["concurrency"]!.GetValue().Should().BeEqualTo(4); + } } diff --git a/v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs b/v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs new file mode 100644 index 00000000..c28ad1da --- /dev/null +++ b/v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs @@ -0,0 +1,164 @@ +using System.Reflection; +using ReactiveUI.Builder; +using ServiceLib.Enums; +using ServiceLib.Models.Entities; +using ServiceLib.Resx; +using ServiceLib.Tests.CoreConfig; +using ServiceLib.ViewModels; + +namespace ServiceLib.Tests.ViewModels; + +[NotInParallel] +public class AddServerViewModelTests +{ + static AddServerViewModelTests() + { + RxAppBuilder.CreateReactiveUIBuilder().WithCoreServices().BuildApp(); + } + + [Test] + public async Task XdriveServices_ShouldContainExpectedServices() + { + await Global.XdriveServices.Count.Should().BeEqualTo(3); + await Global.XdriveServices[0].Should().BeEqualTo("google-drive"); + await Global.XdriveServices[1].Should().BeEqualTo("s3"); + await Global.XdriveServices[2].Should().BeEqualTo("local"); + } + + [Test] + public async Task AddServerViewModel_ShouldBindXdrivePropertiesFromTransportExtra() + { + CoreConfigTestFactory.BindAppManagerConfig(CoreConfigTestFactory.CreateConfig()); + + var profile = new ProfileItem + { + IndexId = "test-node", + ConfigType = EConfigType.VLESS, + Address = "example.com", + Port = 443, + Network = nameof(ETransport.xdrive), + }; + profile.SetTransportExtra(new TransportExtraItem + { + XdriveService = "s3", + XdriveRemoteFolder = "/remote/path", + XdriveSecrets = "sec1,sec2", + XdriveS3Endpoint = "s3.example.com", + XdriveS3Region = "us-west-2", + XdriveS3Bucket = "my-bucket", + XdriveS3PathStyle = true, + XdriveExtra = "{\"concurrency\":4}", + }); + + var vm = new AddServerViewModel(profile); + + await vm.XdriveService.Should().BeEqualTo("s3"); + await vm.XdriveRemoteFolder.Should().BeEqualTo("/remote/path"); + await vm.XdriveSecrets.Should().BeEqualTo("sec1,sec2"); + await vm.XdriveS3Endpoint.Should().BeEqualTo("s3.example.com"); + await vm.XdriveS3Region.Should().BeEqualTo("us-west-2"); + await vm.XdriveS3Bucket.Should().BeEqualTo("my-bucket"); + await vm.XdriveS3PathStyle.Should().BeEqualTo(true); + await vm.XdriveExtra.Should().BeEqualTo("{\"concurrency\":4}"); + } + + [Test] + public async Task AddServerViewModel_TransportFriendlyProperties_ShouldAdaptToXdrive() + { + CoreConfigTestFactory.BindAppManagerConfig(CoreConfigTestFactory.CreateConfig()); + + var profile = new ProfileItem + { + IndexId = "test-node", + ConfigType = EConfigType.VLESS, + Address = "example.com", + Port = 443, + Network = nameof(ETransport.xdrive), + }; + profile.SetTransportExtra(new TransportExtraItem + { + XdriveService = "s3", + XdriveRemoteFolder = "/folder", + XdriveS3Endpoint = "s3.custom.endpoint", + XdriveExtra = "{\"segmentBytes\":1024}", + }); + + var vm = new AddServerViewModel(profile); + + // Getters + await vm.TransportHeaderType.Should().BeEqualTo("s3"); + await vm.TransportHost.Should().BeEqualTo("s3.custom.endpoint"); + await vm.TransportPath.Should().BeEqualTo("/folder"); + await vm.TransportExtraText.Should().BeEqualTo("{\"segmentBytes\":1024}"); + + // Setters + vm.TransportHeaderType = "google-drive"; + await vm.XdriveService.Should().BeEqualTo("google-drive"); + await vm.TransportHeaderType.Should().BeEqualTo("google-drive"); + + vm.TransportHost = "s3.new.endpoint"; + await vm.XdriveS3Endpoint.Should().BeEqualTo("s3.new.endpoint"); + await vm.TransportHost.Should().BeEqualTo("s3.new.endpoint"); + + vm.TransportPath = "/new/folder"; + await vm.XdriveRemoteFolder.Should().BeEqualTo("/new/folder"); + await vm.TransportPath.Should().BeEqualTo("/new/folder"); + + vm.TransportExtraText = "{\"flushIntervalMs\":500}"; + await vm.XdriveExtra.Should().BeEqualTo("{\"flushIntervalMs\":500}"); + await vm.TransportExtraText.Should().BeEqualTo("{\"flushIntervalMs\":500}"); + } + + [Test] + public async Task AddServerViewModel_SaveServerAsync_ShouldPersistXdriveFields() + { + CoreConfigTestFactory.BindAppManagerConfig(CoreConfigTestFactory.CreateConfig()); + + var profile = new ProfileItem + { + IndexId = "test-node", + Remarks = "test remarks", + ConfigType = EConfigType.VLESS, + Address = "example.com", + Port = 443, + Password = Guid.NewGuid().ToString(), + Network = nameof(ETransport.xdrive), + }; + + var vm = new AddServerViewModel(profile); + vm.XdriveService = "s3"; + vm.XdriveRemoteFolder = "/backup"; + vm.XdriveSecrets = "key,secret"; + vm.XdriveS3Endpoint = "s3.amazonaws.com"; + vm.XdriveS3Region = "eu-central-1"; + vm.XdriveS3Bucket = "bucket-1"; + vm.XdriveS3PathStyle = false; + vm.XdriveExtra = "{\"concurrency\":8}"; + + var saveMethod = typeof(AddServerViewModel).GetMethod("SaveServerAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; + await (Task)saveMethod.Invoke(vm, null)!; + + var extra = vm.SelectedSource.GetTransportExtra(); + await extra.XdriveService.Should().BeEqualTo("s3"); + await extra.XdriveRemoteFolder.Should().BeEqualTo("/backup"); + await extra.XdriveSecrets.Should().BeEqualTo("key,secret"); + await extra.XdriveS3Endpoint.Should().BeEqualTo("s3.amazonaws.com"); + await extra.XdriveS3Region.Should().BeEqualTo("eu-central-1"); + await extra.XdriveS3Bucket.Should().BeEqualTo("bucket-1"); + await extra.XdriveS3PathStyle.Should().BeEqualTo(false); + await extra.XdriveExtra.Should().BeEqualTo("{\"concurrency\":8}"); + } + + [Test] + public async Task Localization_ShouldProvideXdriveStrings() + { + await ResUI.XdriveService.IsNotEmpty().Should().BeTrue(); + await ResUI.XdriveRemoteFolder.IsNotEmpty().Should().BeTrue(); + await ResUI.XdriveSecrets.IsNotEmpty().Should().BeTrue(); + await ResUI.XdriveS3Endpoint.IsNotEmpty().Should().BeTrue(); + await ResUI.XdriveS3Region.IsNotEmpty().Should().BeTrue(); + await ResUI.XdriveS3Bucket.IsNotEmpty().Should().BeTrue(); + await ResUI.XdriveS3PathStyle.IsNotEmpty().Should().BeTrue(); + await ResUI.XdriveExtra.IsNotEmpty().Should().BeTrue(); + } +} diff --git a/v2rayN/ServiceLib/Enums/ETransport.cs b/v2rayN/ServiceLib/Enums/ETransport.cs index b315c5f0..61c4c909 100644 --- a/v2rayN/ServiceLib/Enums/ETransport.cs +++ b/v2rayN/ServiceLib/Enums/ETransport.cs @@ -10,5 +10,6 @@ public enum ETransport h2, http, quic, - grpc + grpc, + xdrive } diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index d72ae827..e19fea8f 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -337,7 +337,8 @@ public class Global "kcp", "grpc", "ws", - "httpupgrade" + "httpupgrade", + "xdrive" ]; public static readonly List KcpHeaderTypes = @@ -455,6 +456,18 @@ public class Global "stream-one" ]; + public static readonly List XdriveServices = ["google-drive", "s3", "local"]; + + public static readonly List XdriveProviders = + [ + "Cloudflare R2", + "Google Drive", + "AWS S3 (Global)", + "Yandex Cloud S3", + "ArvanCloud S3", + "Custom S3" + ]; + public static readonly List DomainStrategy = [ "AsIs", diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs index c51f6185..6b2c65ac 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs @@ -321,6 +321,7 @@ public class StreamSettings4Ray public HysteriaSettings4Ray? hysteriaSettings { get; set; } public MasqueSettings4Ray? masqueSettings { get; set; } + public XdriveSettings4Ray? xdriveSettings { get; set; } public JsonObject? finalmask { get; set; } @@ -462,6 +463,28 @@ public class MasqueWarpSettings4Ray public List? address { get; set; } } +public class XdriveSettings4Ray +{ + public string? service { get; set; } + public string? remoteFolder { get; set; } + public List? secrets { get; set; } + public S3Settings4Ray? s3 { get; set; } + public int? segmentBytes { get; set; } + public int? flushIntervalMs { get; set; } + public int? pollIntervalMs { get; set; } + public int? maxPollIntervalMs { get; set; } + public int? eagerWindowMs { get; set; } + public int? concurrency { get; set; } +} + +public class S3Settings4Ray +{ + public string? endpoint { get; set; } + public string? region { get; set; } + public string? bucket { get; set; } + public bool? pathStyle { get; set; } +} + public class Finalmask4Ray { public List? tcp { get; set; } diff --git a/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs b/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs index b454f022..666e9770 100644 --- a/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs @@ -16,4 +16,13 @@ public record TransportExtraItem public string? KcpHeaderType { get; init; } public string? KcpSeed { get; init; } public int? KcpMtu { get; init; } + + public string? XdriveService { get; init; } + public string? XdriveRemoteFolder { get; init; } + public string? XdriveSecrets { get; init; } + public string? XdriveS3Endpoint { get; init; } + public string? XdriveS3Region { get; init; } + public string? XdriveS3Bucket { get; init; } + public bool? XdriveS3PathStyle { get; init; } + public string? XdriveExtra { get; init; } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 64b7b40f..17e0f85f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -996,6 +996,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Columns 的本地化字符串。 + /// + public static string menuColumns { + get { + return ResourceManager.GetString("menuColumns", resourceCulture); + } + } + /// /// 查找类似 Close Connection 的本地化字符串。 /// @@ -1032,15 +1041,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Currently frozen first {0} column(s) 的本地化字符串。 - /// - public static string menuCurrentlyFrozenColumns { - get { - return ResourceManager.GetString("menuCurrentlyFrozenColumns", resourceCulture); - } - } - /// /// 查找类似 Latency Test 的本地化字符串。 /// @@ -1185,15 +1185,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Freeze up to this column 的本地化字符串。 - /// - public static string menuFreezeToThisColumn { - get { - return ResourceManager.GetString("menuFreezeToThisColumn", resourceCulture); - } - } - /// /// 查找类似 Full Config Template Setting 的本地化字符串。 /// @@ -1239,15 +1230,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Hide this column 的本地化字符串。 - /// - public static string menuHideThisColumn { - get { - return ResourceManager.GetString("menuHideThisColumn", resourceCulture); - } - } - /// /// 查找类似 Import Rules From Clipboard 的本地化字符串。 /// @@ -1824,15 +1806,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Show columns 的本地化字符串。 - /// - public static string menuShowColumns { - get { - return ResourceManager.GetString("menuShowColumns", resourceCulture); - } - } - /// /// 查找类似 Show or hide the main window 的本地化字符串。 /// @@ -2022,15 +1995,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Unfreeze all columns 的本地化字符串。 - /// - public static string menuUnfreezeAllColumns { - get { - return ResourceManager.GetString("menuUnfreezeAllColumns", resourceCulture); - } - } - /// /// 查找类似 {0} Website 的本地化字符串。 /// @@ -4949,7 +4913,7 @@ namespace ServiceLib.Resx { return ResourceManager.GetString("TbSettingsTunAutoRoute", resourceCulture); } } - + /// /// 查找类似 DNS mode 的本地化字符串。 /// @@ -5516,5 +5480,113 @@ namespace ServiceLib.Resx { return ResourceManager.GetString("UpgradeAppNotExistTip", resourceCulture); } } + + /// + /// 查找类似 Preset Provider 的本地化字符串。 + /// + public static string XdriveProvider { + get { + return ResourceManager.GetString("XdriveProvider", resourceCulture); + } + } + + /// + /// 查找类似 XDRIVE service 的本地化字符串。 + /// + public static string XdriveService { + get { + return ResourceManager.GetString("XdriveService", resourceCulture); + } + } + + /// + /// 查找类似 Remote folder 的本地化字符串。 + /// + public static string XdriveRemoteFolder { + get { + return ResourceManager.GetString("XdriveRemoteFolder", resourceCulture); + } + } + + /// + /// 查找类似 Secrets (comma separated) 的本地化字符串。 + /// + public static string XdriveSecrets { + get { + return ResourceManager.GetString("XdriveSecrets", resourceCulture); + } + } + + /// + /// 查找类似 S3 Endpoint 的本地化字符串。 + /// + public static string XdriveS3Endpoint { + get { + return ResourceManager.GetString("XdriveS3Endpoint", resourceCulture); + } + } + + /// + /// 查找类似 S3 Region 的本地化字符串。 + /// + public static string XdriveS3Region { + get { + return ResourceManager.GetString("XdriveS3Region", resourceCulture); + } + } + + /// + /// 查找类似 S3 Bucket 的本地化字符串。 + /// + public static string XdriveS3Bucket { + get { + return ResourceManager.GetString("XdriveS3Bucket", resourceCulture); + } + } + + /// + /// 查找类似 S3 Force Path Style 的本地化字符串。 + /// + public static string XdriveS3PathStyle { + get { + return ResourceManager.GetString("XdriveS3PathStyle", resourceCulture); + } + } + + /// + /// 查找类似 XDRIVE Extra 的本地化字符串。 + /// + public static string XdriveExtra { + get { + return ResourceManager.GetString("XdriveExtra", resourceCulture); + } + } + + /// + /// 查找类似 Raw JSON, format: { XdriveObject } 的本地化字符串。 + /// + public static string XdriveExtraTip { + get { + return ResourceManager.GetString("XdriveExtraTip", resourceCulture); + } + } + + /// + /// 查找类似 XDRIVE service 的本地化字符串。 + /// + public static string TransportHeaderType6 { + get { + return ResourceManager.GetString("TransportHeaderType6", resourceCulture); + } + } + + /// + /// 查找类似 XDRIVE remote folder 的本地化字符串。 + /// + public static string TransportPathTip6 { + get { + return ResourceManager.GetString("TransportPathTip6", resourceCulture); + } + } } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.az.resx b/v2rayN/ServiceLib/Resx/ResUI.az.resx index 846f283a..0c547f2f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.az.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.az.resx @@ -1902,4 +1902,40 @@ Təyin edildikdə sertifikat sabitlənəcək və "Təhlükəsiz olmayan əlaqəy Proses + + Təyin olunmuş təchizatçı + + + Saxlama xidməti (service) + + + Uzaqdakı qovluq (remoteFolder) + + + Giriş açarları (AK,SK / OAuth) + + + S3 son nöqtəsi (Endpoint) + + + S3 regionu (Region) + + + S3 səbəti (Bucket) + + + S3 ünvanlama rejimi (PathStyle) + + + XDRIVE əlavə parametrləri + + + Emal edilməmiş JSON, format: { Xdrive obyekti } + + + XDRIVE xidməti + + + XDRIVE uzaqdakı qovluq + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa.resx b/v2rayN/ServiceLib/Resx/ResUI.fa.resx index ce312a5c..d5ca4c3e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa.resx @@ -1776,4 +1776,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if New Update + + ارائه‌دهنده پیش‌فرض + + + سرویس ذخیره‌سازی (service) + + + پوشه ریموت (remoteFolder) + + + کلیدهای دسترسی (AK,SK / OAuth) + + + آدرس اندپوینت S3 + + + منطقه S3 (region) + + + باکت S3 (bucket) + + + آدرس‌دهی مستقیم S3 (PathStyle) + + + تنظیمات پیشرفته XDRIVE + + + فرمت JSON خام: { XdriveObject } + + + سرویس XDRIVE + + + پوشه ریموت XDRIVE + diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 0f85bc63..ffc9065f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1770,4 +1770,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Nouvelle MAJ + + Fournisseur prédéfini + + + Service de stockage (service) + + + Dossier distant (remoteFolder) + + + Clés d'accès (AK,SK / OAuth) + + + Point de terminaison S3 (Endpoint) + + + Région S3 (Region) + + + Bucket S3 + + + Mode d'adressage S3 (PathStyle) + + + XDRIVE Extra + + + JSON brut, format : { XdriveObject } + + + Service XDRIVE + + + Dossier distant XDRIVE + diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 42ac8f20..a7d5301b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1773,4 +1773,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if New Update + + Előre beállított szolgáltató + + + Tárhelyszolgáltatás (service) + + + Távoli mappa (remoteFolder) + + + Hozzáférési kulcsok (AK,SK / OAuth) + + + S3 végpont (Endpoint) + + + S3 régió (Region) + + + S3 tárhely (Bucket) + + + S3 elérési út stílus (PathStyle) + + + XDRIVE Extra + + + Raw JSON, formátum: { XdriveObject } + + + XDRIVE szolgáltatás + + + XDRIVE távoli mappa + diff --git a/v2rayN/ServiceLib/Resx/ResUI.id.resx b/v2rayN/ServiceLib/Resx/ResUI.id.resx index 3fab4a97..8e661692 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.id.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.id.resx @@ -1797,4 +1797,40 @@ Tindakan "Ambil sertifikat" mungkin gagal jika sertifikat self-signed digunakan Memecah bagian akhir paket menjadi fragmen yang lebih kecil. Hal ini dapat memengaruhi throughput dan latensi. + + Penyedia Preset + + + Layanan Penyimpanan (service) + + + Folder Jarak Jauh (remoteFolder) + + + Kredensial Akses (AK,SK / OAuth) + + + Endpoint S3 + + + Wilayah S3 (Region) + + + Bucket S3 + + + Gaya Jalur S3 (PathStyle) + + + XDRIVE Ekstra + + + JSON mentah, format: { XdriveObject } + + + Layanan XDRIVE + + + Folder jarak jauh XDRIVE + diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 0d3bd0c8..f2c38a87 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1917,8 +1917,8 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Enter a valid IPv6 CIDR range with at least two addresses, for example fc00::/18. - - Show columns + + Columns Warp @@ -1926,16 +1926,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Pre-resolve server domains - - Freeze up to this column + + Preset Provider - - Unfreeze all columns + + Storage service (service) - - Currently frozen first {0} column(s) + + Remote folder (remoteFolder) - - Hide this column + + Secrets (AK,SK / OAuth) - \ No newline at end of file + + S3 Endpoint + + + S3 Region + + + S3 Bucket + + + S3 Force Path Style + + + XDRIVE Extra + + + Raw JSON, format: { XdriveObject } + + + XDRIVE service + + + XDRIVE remote folder + + diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index a33b7fc6..d3ed2ebb 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1908,4 +1908,40 @@ Добавить сервер [MASQUE] + + Шаблон провайдера + + + Облачный сервис (service) + + + Удаленная папка (remoteFolder) + + + Ключи доступа (AK,SK / OAuth) + + + Эндпоинт S3 (Endpoint) + + + Регион S3 (Region) + + + Бакет S3 (Bucket) + + + Режим адресации S3 (PathStyle) + + + Дополнительные параметры XDRIVE (Extra) + + + Сырой JSON, формат: { XdriveObject } + + + XDRIVE-сервис + + + Удаленная папка XDRIVE + diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index f6220afc..6d88cf80 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1909,8 +1909,8 @@ 请输入至少包含两个地址的有效 IPv6 网段,例如 fc00::/18。 - - 显示列 + + 列 Warp @@ -1918,16 +1918,40 @@ 预解析节点域名 - - 冻结至此列 + + 预设服务商 - - 取消所有冻结 + + 存储服务 (service) - - 当前已冻结前 {0} 列 + + 远程目录 (remoteFolder) - - 隐藏此列 + + 访问凭证 Secrets (AK,SK / OAuth) - \ No newline at end of file + + S3 终端节点 + + + S3 区域 + + + S3 存储桶 + + + S3 强制路径样式 (PathStyle) + + + XDRIVE 额外配置 + + + 原始 JSON,格式: { XdriveObject } + + + XDRIVE 服务 + + + XDRIVE 远端目录 + + diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index b4dcf6b3..9ad71131 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1927,4 +1927,40 @@ 請輸入至少包含兩個位址的有效 IPv6 網段,例如 fc00::/18。 + + 預設服務商 + + + 存儲服務 (service) + + + 遠端目錄 (remoteFolder) + + + 存取憑證 Secrets (AK,SK / OAuth) + + + S3 終端節點 + + + S3 區域 + + + S3 儲存桶 + + + S3 強制路徑樣式 (PathStyle) + + + XDRIVE 額外配置 + + + 原始 JSON,格式: { XdriveObject } + + + XDRIVE 服務 + + + XDRIVE 遠端目錄 + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 8ca56dbe..9e252c0a 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -506,6 +506,81 @@ public partial class CoreConfigV2rayService streamSettings.grpcSettings = grpcSettings; break; + case nameof(ETransport.xdrive): + streamSettings.network = nameof(ETransport.xdrive); + var xdriveSettings = new XdriveSettings4Ray + { + service = transport.XdriveService, + remoteFolder = transport.XdriveRemoteFolder, + secrets = transport.XdriveSecrets.IsNotEmpty() ? Utils.String2List(transport.XdriveSecrets) : null, + }; + + if (string.Equals(transport.XdriveService, "s3", StringComparison.OrdinalIgnoreCase) + || transport.XdriveS3Endpoint.IsNotEmpty() + || transport.XdriveS3Bucket.IsNotEmpty()) + { + xdriveSettings.s3 = new S3Settings4Ray + { + endpoint = transport.XdriveS3Endpoint, + region = transport.XdriveS3Region, + bucket = transport.XdriveS3Bucket, + pathStyle = transport.XdriveS3PathStyle, + }; + } + + if (transport.XdriveExtra.IsNotEmpty()) + { + try + { + var extraNode = JsonUtils.ParseJson(transport.XdriveExtra); + if (extraNode is JsonObject extraObj) + { + if (int.TryParse(extraObj["segmentBytes"]?.ToString(), out var segmentBytes)) + { + xdriveSettings.segmentBytes = segmentBytes; + } + if (int.TryParse(extraObj["flushIntervalMs"]?.ToString(), out var flushIntervalMs)) + { + xdriveSettings.flushIntervalMs = flushIntervalMs; + } + if (int.TryParse(extraObj["pollIntervalMs"]?.ToString(), out var pollIntervalMs)) + { + xdriveSettings.pollIntervalMs = pollIntervalMs; + } + if (int.TryParse(extraObj["maxPollIntervalMs"]?.ToString(), out var maxPollIntervalMs)) + { + xdriveSettings.maxPollIntervalMs = maxPollIntervalMs; + } + if (int.TryParse(extraObj["eagerWindowMs"]?.ToString(), out var eagerWindowMs)) + { + xdriveSettings.eagerWindowMs = eagerWindowMs; + } + if (int.TryParse(extraObj["concurrency"]?.ToString(), out var concurrency)) + { + xdriveSettings.concurrency = concurrency; + } + } + } + catch + { + // ignore extra json parse errors + } + } + + if (string.Equals(transport.XdriveService, "google-drive", StringComparison.OrdinalIgnoreCase)) + { + xdriveSettings.segmentBytes ??= 524288; + xdriveSettings.flushIntervalMs ??= 10; + xdriveSettings.pollIntervalMs ??= 50; + xdriveSettings.maxPollIntervalMs ??= 200; + xdriveSettings.eagerWindowMs ??= 10000; + xdriveSettings.concurrency ??= 64; + } + + streamSettings.xdriveSettings = xdriveSettings; + FillOutboundMux(outbound, true, true); + break; + case "hysteria": var ports = protocolExtra?.Ports; int? upMbps = protocolExtra?.UpMbps is { } su and >= 0 diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index c76c69ee..76965f5a 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -148,6 +148,33 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable [Reactive] public partial int? KcpMtu { get; set; } + [Reactive] + public partial string XdriveProvider { get; set; } + + [Reactive] + public partial string XdriveService { get; set; } + + [Reactive] + public partial string XdriveRemoteFolder { get; set; } + + [Reactive] + public partial string XdriveSecrets { get; set; } + + [Reactive] + public partial string XdriveS3Endpoint { get; set; } + + [Reactive] + public partial string XdriveS3Region { get; set; } + + [Reactive] + public partial string XdriveS3Bucket { get; set; } + + [Reactive] + public partial bool? XdriveS3PathStyle { get; set; } + + [Reactive] + public partial string XdriveExtra { get; set; } + public string TransportHeaderType { get => SelectedSource.GetNetwork() switch @@ -156,6 +183,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable nameof(ETransport.kcp) => KcpHeaderType, nameof(ETransport.xhttp) => XhttpMode, nameof(ETransport.grpc) => GrpcMode, + nameof(ETransport.xdrive) => XdriveService, _ => string.Empty, }; set @@ -177,6 +205,10 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable case nameof(ETransport.grpc): GrpcMode = value; break; + + case nameof(ETransport.xdrive): + XdriveService = value; + break; } this.RaisePropertyChanged(); } @@ -191,6 +223,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable nameof(ETransport.httpupgrade) => Host, nameof(ETransport.xhttp) => Host, nameof(ETransport.grpc) => GrpcAuthority, + nameof(ETransport.xdrive) => XdriveS3Endpoint, _ => string.Empty, }; set @@ -207,6 +240,10 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable case nameof(ETransport.grpc): GrpcAuthority = value; break; + + case nameof(ETransport.xdrive): + XdriveS3Endpoint = value; + break; } this.RaisePropertyChanged(); } @@ -221,6 +258,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable nameof(ETransport.httpupgrade) => Path, nameof(ETransport.xhttp) => Path, nameof(ETransport.grpc) => GrpcServiceName, + nameof(ETransport.xdrive) => XdriveRemoteFolder, _ => string.Empty, }; set @@ -240,6 +278,10 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable case nameof(ETransport.grpc): GrpcServiceName = value; break; + + case nameof(ETransport.xdrive): + XdriveRemoteFolder = value; + break; } this.RaisePropertyChanged(); } @@ -247,14 +289,23 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable public string TransportExtraText { - get => SelectedSource.GetNetwork() == nameof(ETransport.xhttp) - ? XhttpExtra - : string.Empty; + get => SelectedSource.GetNetwork() switch + { + nameof(ETransport.xhttp) => XhttpExtra, + nameof(ETransport.xdrive) => XdriveExtra, + _ => string.Empty, + }; set { - if (SelectedSource.GetNetwork() == nameof(ETransport.xhttp)) + switch (SelectedSource.GetNetwork()) { - XhttpExtra = value; + case nameof(ETransport.xhttp): + XhttpExtra = value; + break; + + case nameof(ETransport.xdrive): + XdriveExtra = value; + break; } this.RaisePropertyChanged(); } @@ -287,8 +338,16 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable .Subscribe(_ => UpdateCertTip()); this.WhenAnyValue(x => x.Network) - .Subscribe(_ => + .Subscribe(network => { + if (SelectedSource != null && !network.IsNullOrEmpty()) + { + SelectedSource.Network = network; + } + if (network == nameof(ETransport.xdrive)) + { + MuxEnabled = true; + } this.RaisePropertyChanged(nameof(TransportHeaderType)); this.RaisePropertyChanged(nameof(TransportHost)); this.RaisePropertyChanged(nameof(TransportPath)); @@ -298,6 +357,13 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable this.WhenAnyValue(x => x.Cert) .Subscribe(_ => UpdateCertSha()); + this.WhenAnyValue(x => x.XdriveProvider) + .Where(p => p.IsNotEmpty()) + .Subscribe(provider => + { + ApplyXdriveProviderPreset(provider); + }); + if (profileItem.IndexId.IsNullOrEmpty()) { profileItem.Network = Global.DefaultNetwork; @@ -358,6 +424,48 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable KcpHeaderType = transport.KcpHeaderType.IsNullOrEmpty() ? Global.None : transport.KcpHeaderType; KcpSeed = transport.KcpSeed ?? string.Empty; KcpMtu = transport.KcpMtu; + XdriveService = transport.XdriveService.IsNullOrEmpty() ? (Global.XdriveServices.FirstOrDefault() ?? "google-drive") : transport.XdriveService; + XdriveRemoteFolder = transport.XdriveRemoteFolder ?? string.Empty; + XdriveSecrets = transport.XdriveSecrets ?? string.Empty; + XdriveS3Endpoint = transport.XdriveS3Endpoint ?? string.Empty; + XdriveS3Region = transport.XdriveS3Region ?? string.Empty; + XdriveS3Bucket = transport.XdriveS3Bucket ?? string.Empty; + XdriveS3PathStyle = transport.XdriveS3PathStyle; + XdriveExtra = transport.XdriveExtra ?? string.Empty; + + if (transport.XdriveService == "google-drive") + { + XdriveProvider = "Google Drive"; + } + else if (transport.XdriveS3Endpoint?.Contains("r2.cloudflarestorage.com") == true) + { + XdriveProvider = "Cloudflare R2"; + } + else if (transport.XdriveS3Endpoint?.Contains("amazonaws.com") == true) + { + XdriveProvider = "AWS S3 (Global)"; + } + else if (transport.XdriveS3Endpoint?.Contains("yandexcloud.net") == true) + { + XdriveProvider = "Yandex Cloud S3"; + } + else if (transport.XdriveS3Endpoint?.Contains("arvanstorage.ir") == true) + { + XdriveProvider = "ArvanCloud S3"; + } + else if (SelectedSource.Remarks?.Contains("Cloudflare", StringComparison.OrdinalIgnoreCase) == true + || (transport.XdriveS3PathStyle == true && transport.XdriveS3Region == "auto")) + { + XdriveProvider = "Cloudflare R2"; + } + else if (transport.XdriveService == "s3" || transport.XdriveS3Endpoint.IsNotEmpty()) + { + XdriveProvider = "Custom S3"; + } + else + { + XdriveProvider = "Cloudflare R2"; + } } private async Task SaveServerAsync() @@ -440,6 +548,14 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable KcpHeaderType = KcpHeaderType.NullIfEmpty(), KcpSeed = KcpSeed.NullIfEmpty(), KcpMtu = KcpMtu > 0 ? KcpMtu : null, + XdriveService = XdriveService.NullIfEmpty(), + XdriveRemoteFolder = XdriveRemoteFolder.NullIfEmpty(), + XdriveSecrets = XdriveSecrets.NullIfEmpty(), + XdriveS3Endpoint = XdriveS3Endpoint.NullIfEmpty(), + XdriveS3Region = XdriveS3Region.NullIfEmpty(), + XdriveS3Bucket = XdriveS3Bucket.NullIfEmpty(), + XdriveS3PathStyle = XdriveS3PathStyle, + XdriveExtra = XdriveExtra.NullIfEmpty(), }; SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with @@ -494,6 +610,44 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable : errorMessage; } + private void ApplyXdriveProviderPreset(string provider) + { + switch (provider) + { + case "Cloudflare R2": + XdriveService = "s3"; + if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "auto"; + XdriveS3PathStyle = true; + break; + case "Google Drive": + XdriveService = "google-drive"; + SelectedSource.StreamSecurity = Global.StreamSecurity; + if (SelectedSource.Sni.IsNullOrEmpty()) SelectedSource.Sni = "www.google.com"; + if (SelectedSource.Alpn.IsNullOrEmpty()) SelectedSource.Alpn = "h2"; + if (SelectedSource.Address.IsNullOrEmpty()) SelectedSource.Address = "www.google.com"; + if (SelectedSource.Port <= 0) SelectedSource.Port = 443; + break; + case "AWS S3 (Global)": + XdriveService = "s3"; + if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ap-northeast-1"; + XdriveS3PathStyle = false; + break; + case "Yandex Cloud S3": + XdriveService = "s3"; + if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ru-central1"; + XdriveS3PathStyle = true; + break; + case "ArvanCloud S3": + XdriveService = "s3"; + if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ir-tbz-sh1"; + XdriveS3PathStyle = true; + break; + case "Custom S3": + XdriveService = "s3"; + break; + } + } + private void UpdateCertSha() { if (Cert.IsNullOrEmpty()) @@ -582,6 +736,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable nameof(ETransport.httpupgrade) => Host, nameof(ETransport.xhttp) => Host, nameof(ETransport.grpc) => GrpcAuthority, + nameof(ETransport.xdrive) => XdriveS3Endpoint, _ => string.Empty, }; } diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index ce29e9de..aad4e3b5 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -1050,6 +1050,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + this.Bind(ViewModel, vm => vm.GrpcAuthority, v => v.txtRequestHostGrpc.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.GrpcServiceName, v => v.txtPathGrpc.Text).DisposeWith(disposables); + cmbProviderXdrive.ItemsSource = Global.XdriveProviders; + this.Bind(ViewModel, vm => vm.XdriveProvider, v => v.cmbProviderXdrive.SelectedValue).DisposeWith(disposables); + + cmbServiceXdrive.ItemsSource = Global.XdriveServices; + this.Bind(ViewModel, vm => vm.XdriveService, v => v.cmbServiceXdrive.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XdriveRemoteFolder, v => v.txtRemoteFolderXdrive.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XdriveSecrets, v => v.txtSecretsXdrive.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XdriveS3Endpoint, v => v.txtEndpointXdrive.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XdriveS3Bucket, v => v.txtBucketXdrive.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XdriveS3Region, v => v.txtRegionXdrive.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XdriveS3PathStyle, v => v.togPathStyleXdrive.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XdriveExtra, v => v.txtExtraXdrive.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.StreamSecurity, v => v.cmbStreamSecurity.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AllowInsecure, v => v.togAllowInsecure.IsChecked).DisposeWith(disposables); @@ -359,6 +372,7 @@ public partial class AddServerWindow : WindowBase gridTransportHttpupgrade.IsVisible = false; gridTransportXhttp.IsVisible = false; gridTransportGrpc.IsVisible = false; + gridTransportXdrive.IsVisible = false; switch (network) { @@ -386,6 +400,10 @@ public partial class AddServerWindow : WindowBase gridTransportGrpc.IsVisible = true; break; + case nameof(ETransport.xdrive): + gridTransportXdrive.IsVisible = true; + break; + default: gridTransportRaw.IsVisible = true; break;