From bbd07f6badd0df827ba945428acce84c3e09d2c4 Mon Sep 17 00:00:00 2001 From: dranik Date: Tue, 24 Mar 2026 10:33:41 +0300 Subject: [PATCH 01/18] update UI XRay, add new page PageProtocolXrayTransportSettings.qml PageProtocolXrayXmuxSettings.qml PageProtocolXrayXPaddingSettings.qml --- client/ui/controllers/qml/pageController.h | 6 +- client/ui/qml/Controls2/MinMaxRowType.qml | 61 ++ .../qml/Pages2/PageProtocolXraySettings.qml | 176 ++++- .../PageProtocolXrayTransportSettings.qml | 702 ++++++++++++++++++ .../PageProtocolXrayXPaddingSettings.qml | 211 ++++++ .../Pages2/PageProtocolXrayXmuxSettings.qml | 219 ++++++ client/ui/qml/qml.qrc | 6 + 7 files changed, 1339 insertions(+), 42 deletions(-) create mode 100644 client/ui/qml/Controls2/MinMaxRowType.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 603e8a8f4..57362aa6a 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -80,7 +80,11 @@ namespace PageLoader PageSetupWizardApiPremiumInfo, PageSetupWizardApiTrialEmail, - PageDevMenu + PageDevMenu, + + PageProtocolXrayTransportSettings, + PageProtocolXrayXmuxSettings, + PageProtocolXrayXPaddingSettings, }; Q_ENUM_NS(PageEnum) diff --git a/client/ui/qml/Controls2/MinMaxRowType.qml b/client/ui/qml/Controls2/MinMaxRowType.qml new file mode 100644 index 000000000..130de293a --- /dev/null +++ b/client/ui/qml/Controls2/MinMaxRowType.qml @@ -0,0 +1,61 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" + +// MinMaxRowType — two side-by-side labeled text fields: Min / Max +// Usage: +// MinMaxRowType { +// minValue: "0" +// maxValue: "0" +// onMinChanged: someProperty = val +// onMaxChanged: someProperty = val +// } +Item { + id: root + + property string minValue: "0" + property string maxValue: "0" + + signal minChanged(string val) + signal maxChanged(string val) + + implicitHeight: row.implicitHeight + implicitWidth: row.implicitWidth + + RowLayout { + id: row + anchors.fill: parent + spacing: 8 + + // Min field + TextFieldWithHeaderType { + Layout.fillWidth: true + headerText: qsTr("Min") + textField.text: root.minValue + textField.validator: IntValidator { bottom: 0 } + textField.onEditingFinished: { + if (textField.text !== root.minValue) { + root.minChanged(textField.text) + } + } + } + + // Max field + TextFieldWithHeaderType { + Layout.fillWidth: true + headerText: qsTr("Max") + textField.text: root.maxValue + textField.validator: IntValidator { bottom: 0 } + textField.onEditingFinished: { + if (textField.text !== root.maxValue) { + root.maxChanged(textField.text) + } + } + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 552ec3794..dd9455c9c 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -46,19 +46,52 @@ PageType { delegate: ColumnLayout { width: listView.width - property alias focusItemId: textFieldWithHeaderType.textField + property alias focusItemId: portTextField.textField spacing: 0 - BaseHeaderType { + // ── Header ──────────────────────────────────────────────── + RowLayout { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - headerText: qsTr("XRay settings") + Layout.topMargin: 0 + + Header2TextType { + Layout.fillWidth: true + text: qsTr("XRay\nVLESS") + wrapMode: Text.WordWrap + } + + ImageButtonType { + Layout.alignment: Qt.AlignTop | Qt.AlignRight + implicitWidth: 40 + implicitHeight: 40 + image: "qrc:/images/controls/more-vertical.svg" + imageColor: AmneziaStyle.color.mutedGray + } } + // ── "More about settings" link ──────────────────────────── + LabelTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 4 + + text: qsTr("More about settings") + color: AmneziaStyle.color.burntOrange + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://docs.amnezia.org") + } + } + + // ── Port field ──────────────────────────────────────────── TextFieldWithHeaderType { - id: textFieldWithHeaderType + id: portTextField Layout.fillWidth: true Layout.topMargin: 32 @@ -67,40 +100,12 @@ PageType { enabled: listView.enabled - headerText: qsTr("Disguised as traffic from") - textField.text: site - - textField.onEditingFinished: { - if (textField.text !== site) { - var tmpText = textField.text - tmpText = tmpText.toLocaleLowerCase() - - if (tmpText.startsWith("https://")) { - tmpText = textField.text.substring(8) - site = tmpText - } else { - site = textField.text - } - } - } - - checkEmptyText: true - } - - TextFieldWithHeaderType { - id: portTextField - - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - enabled: listView.enabled - headerText: qsTr("Port") textField.text: port textField.maximumLength: 5 - textField.validator: IntValidator { bottom: 1; top: 65535 } + textField.validator: IntValidator { + bottom: 1; top: 65535 + } textField.onEditingFinished: { if (textField.text !== port) { @@ -111,12 +116,70 @@ PageType { checkEmptyText: true } + // ── Transport row ───────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + + text: qsTr("Transport") + descriptionText: "RAW (TCP)" // TODO: model role + rightImageSource: "qrc:/images/controls/chevron-right.svg" + enabled: listView.enabled + + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings) + } + } + + DividerType { + } + + // ── Security row ────────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Security") + descriptionText: "TLS" // TODO: model role + rightImageSource: "qrc:/images/controls/chevron-right.svg" + enabled: listView.enabled + + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings) + } + } + + DividerType { + } + + // ── Flow row ────────────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Flow") + descriptionText: "xtls-rprx-vision" // TODO: model role + rightImageSource: "qrc:/images/controls/chevron-right.svg" + enabled: listView.enabled + + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings) + } + } + + DividerType { + } + + // ── Spacer ──────────────────────────────────────────────── + Item { + Layout.fillWidth: true + Layout.preferredHeight: 24 + } + + // ── Save button ─────────────────────────────────────────── BasicButtonType { id: saveButton Layout.fillWidth: true - Layout.topMargin: 24 - Layout.bottomMargin: 24 + Layout.bottomMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 @@ -124,7 +187,7 @@ PageType { text: qsTr("Save") - onClicked: function() { + onClicked: function () { forceActiveFocus() var headerText = qsTr("Save settings?") @@ -132,16 +195,16 @@ PageType { var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") - var yesButtonFunction = function() { + var yesButtonFunction = function () { if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) return } - PageController.goToPage(PageEnum.PageSetupWizardInstalling); + PageController.goToPage(PageEnum.PageSetupWizardInstalling) InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) } - var noButtonFunction = function() { + var noButtonFunction = function () { if (!GC.isMobile()) { saveButton.forceActiveFocus() } @@ -152,6 +215,37 @@ PageType { Keys.onEnterPressed: saveButton.clicked() Keys.onReturnPressed: saveButton.clicked() } + + // ── Reset settings ──────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + + text: qsTr("Reset settings") + textColor: AmneziaStyle.color.vibrantRed + visible: listView.enabled + + clickedFunction: function () { + var headerText = qsTr("Reset settings?") + var descriptionText = qsTr("All XRay settings will be restored to defaults.") + var yesButtonText = qsTr("Reset") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function () { + XrayConfigModel.resetToDefaults() + } + var noButtonFunction = function () { + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, + yesButtonFunction, noButtonFunction) + } + } + + // ── Bottom padding ──────────────────────────────────────── + Item { + Layout.fillWidth: true + Layout.preferredHeight: 32 + } } } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml new file mode 100644 index 000000000..093990fdb --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -0,0 +1,702 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + // Temporary local state — will be replaced by model roles + property int selectedTransport: 0 // 0=RAW, 1=XHTTP, 2=mKCP + + // XHTTP fields + property string xhttpMode: "Auto" + property string xhttpHost: "www.googletagmanager.com" + property string xhttpPath: "" + property string xhttpHeadersTemplate: "HTTP" + property string xhttpUplinkMethod: "POST" + property bool xhttpDisableGrpc: true + property bool xhttpDisableSse: true + // Session & Sequence + property string sessionPlacement: "Path" + property string sessionKey: "Path" + property string seqPlacement: "Path" + property string seqKey: "" + property string uplinkDataPlacement: "Body" + property string uplinkDataKey: "" + // Traffic Shaping + property string uplinkChunkSize: "0" + property string scMaxBufferedPosts: "" + // mKCP fields + property string mkcpTti: "" + property string mkcpUplinkCapacity: "" + property string mkcpDownlinkCapacity: "" + property string mkcpReadBufferSize: "" + property string mkcpWriteBufferSize: "" + property bool mkcpCongestion: true + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + FlickableType { + id: flickable + anchors.top: backButton.bottom + anchors.bottom: saveButton.top + anchors.left: parent.left + anchors.right: parent.right + contentHeight: mainColumn.implicitHeight + + ColumnLayout { + id: mainColumn + width: flickable.width + spacing: 0 + + // ── Header ──────────────────────────────────────────────── + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + text: qsTr("Transport") + } + + // ── Radio: RAW (TCP) ────────────────────────────────────── + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("RAW (TCP)") + checked: root.selectedTransport === 0 + onClicked: root.selectedTransport = 0 + } + + DividerType { + } + + // ── Radio: XHTTP ────────────────────────────────────────── + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("XHTTP") + descriptionText: qsTr("Advanced users") + checked: root.selectedTransport === 1 + onClicked: root.selectedTransport = 1 + } + + DividerType { + } + + // ── Radio: mKCP ─────────────────────────────────────────── + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("mKCP") + checked: root.selectedTransport === 2 + onClicked: root.selectedTransport = 2 + } + + DividerType { + } + + // ══════════════════════════════════════════════════════════ + // mKCP Settings (visible when mKCP selected) + // ══════════════════════════════════════════════════════════ + ColumnLayout { + visible: root.selectedTransport === 2 + Layout.fillWidth: true + spacing: 0 + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("mKCP Settings") + color: AmneziaStyle.color.mutedGray + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("TTI") + textField.text: root.mkcpTti + textField.onEditingFinished: root.mkcpTti = textField.text + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("uplinkCapacity") + textField.text: root.mkcpUplinkCapacity + textField.onEditingFinished: root.mkcpUplinkCapacity = textField.text + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("downlinkCapacity") + textField.text: root.mkcpDownlinkCapacity + textField.onEditingFinished: root.mkcpDownlinkCapacity = textField.text + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("readBufferSize") + textField.text: root.mkcpReadBufferSize + textField.onEditingFinished: root.mkcpReadBufferSize = textField.text + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("writeBufferSize") + textField.text: root.mkcpWriteBufferSize + textField.onEditingFinished: root.mkcpWriteBufferSize = textField.text + } + + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + Layout.topMargin: 8 + text: qsTr("Congestion") + checked: root.mkcpCongestion + onToggled: root.mkcpCongestion = checked + } + } + + // ══════════════════════════════════════════════════════════ + // XHTTP Settings (visible when XHTTP selected) + // ══════════════════════════════════════════════════════════ + ColumnLayout { + visible: root.selectedTransport === 1 + Layout.fillWidth: true + spacing: 0 + + // Mode dropdown + DropDownType { + id: modeDropDown + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.xhttpMode + descriptionText: qsTr("Mode") + headerText: qsTr("Mode") + drawerParent: root + listView: ListViewWithRadioButtonType { + id: modeListView + rootWidth: root.width + model: ListModel { + ListElement { + name: "Auto" + } + ListElement { + name: "Packet-up" + } + ListElement { + name: "Stream-up" + } + ListElement { + name: "Stream-one" + } + } + clickedFunction: function () { + root.xhttpMode = selectedText + modeDropDown.text = selectedText + modeDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.xhttpMode) { + selectedIndex = i; + break + } + } + } + } + } + + // HTTP Profile label + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("HTTP Profile") + color: AmneziaStyle.color.mutedGray + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Host") + textField.text: root.xhttpHost + textField.onEditingFinished: root.xhttpHost = textField.text + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Path") + textField.text: root.xhttpPath + textField.onEditingFinished: root.xhttpPath = textField.text + } + + // Headers template dropdown + DropDownType { + id: headersDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.xhttpHeadersTemplate + descriptionText: qsTr("Headers template") + headerText: qsTr("Headers template") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "HTTP" + } + ListElement { + name: "None" + } + } + clickedFunction: function () { + root.xhttpHeadersTemplate = selectedText + headersDropDown.text = selectedText + headersDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.xhttpHeadersTemplate) { + selectedIndex = i; + break + } + } + } + } + } + + // UplinkHTTPMethod dropdown + DropDownType { + id: uplinkMethodDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.xhttpUplinkMethod + descriptionText: qsTr("UplinkHTTPMethod") + headerText: qsTr("UplinkHTTPMethod") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "POST" + } + ListElement { + name: "PUT" + } + ListElement { + name: "PATCH" + } + } + clickedFunction: function () { + root.xhttpUplinkMethod = selectedText + uplinkMethodDropDown.text = selectedText + uplinkMethodDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.xhttpUplinkMethod) { + selectedIndex = i; + break + } + } + } + } + } + + // Disable gRPC Header + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + Layout.topMargin: 16 + text: qsTr("Disable gRPC Header") + descriptionText: qsTr("noGRPCHeader") + checked: root.xhttpDisableGrpc + onToggled: root.xhttpDisableGrpc = checked + } + + DividerType { + } + + // Disable SSE Header + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + text: qsTr("Disable SSE Header") + descriptionText: qsTr("noSSEHeader") + checked: root.xhttpDisableSse + onToggled: root.xhttpDisableSse = checked + } + + DividerType { + } + + // ── Session & Sequence ──────────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Session & Sequence") + color: AmneziaStyle.color.mutedGray + } + + DropDownType { + id: sessionPlacementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.sessionPlacement + descriptionText: qsTr("SessionPlacement") + headerText: qsTr("SessionPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Path" + } + ListElement { + name: "Header" + } + ListElement { + name: "Cookie" + } + ListElement { + name: "None" + } + } + clickedFunction: function () { + root.sessionPlacement = selectedText + sessionPlacementDropDown.text = selectedText + sessionPlacementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.sessionPlacement) { + selectedIndex = i; + break + } + } + } + } + } + + DropDownType { + id: sessionKeyDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.sessionKey + descriptionText: qsTr("SessionKey") + headerText: qsTr("SessionKey") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Path" + } + ListElement { + name: "Header" + } + ListElement { + name: "None" + } + } + clickedFunction: function () { + root.sessionKey = selectedText + sessionKeyDropDown.text = selectedText + sessionKeyDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.sessionKey) { + selectedIndex = i; + break + } + } + } + } + } + + DropDownType { + id: seqPlacementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.seqPlacement + descriptionText: qsTr("SeqPlacement") + headerText: qsTr("SeqPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Path" + } + ListElement { + name: "Header" + } + ListElement { + name: "Cookie" + } + ListElement { + name: "None" + } + } + clickedFunction: function () { + root.seqPlacement = selectedText + seqPlacementDropDown.text = selectedText + seqPlacementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.seqPlacement) { + selectedIndex = i; + break + } + } + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("SeqKey") + textField.text: root.seqKey + textField.onEditingFinished: root.seqKey = textField.text + } + + DropDownType { + id: uplinkDataPlacementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.uplinkDataPlacement + descriptionText: qsTr("UplinkDataPlacement") + headerText: qsTr("UplinkDataPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Body" + } + ListElement { + name: "Query" + } + } + clickedFunction: function () { + root.uplinkDataPlacement = selectedText + uplinkDataPlacementDropDown.text = selectedText + uplinkDataPlacementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.uplinkDataPlacement) { + selectedIndex = i; + break + } + } + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("UplinkDataKey") + textField.text: root.uplinkDataKey + textField.onEditingFinished: root.uplinkDataKey = textField.text + } + + // ── Traffic Shaping ─────────────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Traffic Shaping") + color: AmneziaStyle.color.mutedGray + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("UplinkChunkSize") + textField.text: root.uplinkChunkSize + textField.validator: IntValidator { + bottom: 0 + } + textField.onEditingFinished: root.uplinkChunkSize = textField.text + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("scMaxBufferedPosts") + textField.text: root.scMaxBufferedPosts + textField.onEditingFinished: root.scMaxBufferedPosts = textField.text + } + + // scMaxEachPostBytes → nav row + LabelWithButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + text: qsTr("scMaxEachPostBytes") + descriptionText: qsTr("1—100") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { /* navigate */ + } + } + DividerType { + } + + // scMinPostsIntervalMs → nav row + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("scMinPostsIntervalMs") + descriptionText: qsTr("100—600ms") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { /* navigate */ + } + } + DividerType { + } + + // scStreamUpServerSecs → nav row + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("scStreamUpServerSecs") + descriptionText: qsTr("1—100") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { /* navigate */ + } + } + DividerType { + } + + // ── Padding and multiplexing ────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Padding and multiplexing") + color: AmneziaStyle.color.mutedGray + } + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("xPadding") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings) + } + } + DividerType { + } + + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("XMux") + descriptionText: qsTr("On") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings) + } + } + DividerType { + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + // ── Save button ─────────────────────────────────────────────────── + BasicButtonType { + id: saveButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + text: qsTr("Save") + onClicked: { + forceActiveFocus() + // XrayConfigModel.setTransport(...) + } + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml new file mode 100644 index 000000000..e99045697 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -0,0 +1,211 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + // Temporary local state + property string xPaddingBytes: "0—0" + property bool xPaddingObfsMode: true + property string xPaddingKey: "www.googletagmanager.com" + property string xPaddingHeader: "" + property string xPaddingPlacement: "Cookie" + property string xPaddingMethod: "Repeat-x" + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + FlickableType { + id: flickable + anchors.top: backButton.bottom + anchors.bottom: saveButton.top + anchors.left: parent.left + anchors.right: parent.right + contentHeight: mainColumn.implicitHeight + + ColumnLayout { + id: mainColumn + width: flickable.width + spacing: 0 + + // ── Header ──────────────────────────────────────────────── + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + text: qsTr("xPadding") + } + + // ── xPaddingBytes nav row ───────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("xPaddingBytes") + descriptionText: root.xPaddingBytes + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings) + } + } + + DividerType { + } + + // ── xPaddingObfsMode switcher ───────────────────────────── + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + text: qsTr("xPaddingObfsMode") + checked: root.xPaddingObfsMode + onToggled: root.xPaddingObfsMode = checked + } + + DividerType { + } + + // ── xPaddingKey ─────────────────────────────────────────── + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + headerText: qsTr("xPaddingKey") + textField.text: root.xPaddingKey + textField.onEditingFinished: root.xPaddingKey = textField.text + } + + // ── xPaddingHeader ──────────────────────────────────────── + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("xPaddingHeader") + textField.text: root.xPaddingHeader + textField.onEditingFinished: root.xPaddingHeader = textField.text + } + + // ── xPaddingPlacement dropdown ──────────────────────────── + DropDownType { + id: placementDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.xPaddingPlacement + descriptionText: qsTr("xPaddingPlacement") + headerText: qsTr("xPaddingPlacement") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Cookie" + } + ListElement { + name: "Header" + } + ListElement { + name: "Query" + } + ListElement { + name: "Body" + } + } + clickedFunction: function () { + root.xPaddingPlacement = selectedText + placementDropDown.text = selectedText + placementDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.xPaddingPlacement) { + selectedIndex = i; + break + } + } + } + } + } + + // ── xPaddingMethod dropdown ─────────────────────────────── + DropDownType { + id: methodDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.xPaddingMethod + descriptionText: qsTr("xPaddingMethod") + headerText: qsTr("xPaddingMethod") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Repeat-x" + } + ListElement { + name: "Random" + } + ListElement { + name: "Zero" + } + } + clickedFunction: function () { + root.xPaddingMethod = selectedText + methodDropDown.text = selectedText + methodDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.xPaddingMethod) { + selectedIndex = i; + break + } + } + } + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + // ── Save button ─────────────────────────────────────────────────── + BasicButtonType { + id: saveButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + text: qsTr("Save") + onClicked: { + forceActiveFocus() + // XrayConfigModel.setXPadding(...) + } + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml new file mode 100644 index 000000000..e998e55f3 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -0,0 +1,219 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + // Temporary local state + property bool xmuxEnabled: true + property string maxConcurrencyMin: "0" + property string maxConcurrencyMax: "0" + property string maxConnectionsMin: "0" + property string maxConnectionsMax: "0" + property string cMaxReuseTimesMin: "0" + property string cMaxReuseTimesMax: "0" + property string hMaxRequestTimesMin: "0" + property string hMaxRequestTimesMax: "0" + property string hMaxReusableSecsMin: "0" + property string hMaxReusableSecsMax: "0" + property string hKeepAlivePeriod: "" + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + FlickableType { + id: flickable + anchors.top: backButton.bottom + anchors.bottom: saveButton.top + anchors.left: parent.left + anchors.right: parent.right + contentHeight: mainColumn.implicitHeight + + ColumnLayout { + id: mainColumn + width: flickable.width + spacing: 0 + + // ── Header ──────────────────────────────────────────────── + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + text: qsTr("xmux") + } + + // ── xmux master switcher ────────────────────────────────── + SwitcherType { + Layout.fillWidth: true + Layout.margins: 16 + text: qsTr("xmux") + checked: root.xmuxEnabled + onToggled: root.xmuxEnabled = checked + } + + DividerType { + } + + // ── Min/Max pairs (only when enabled) ───────────────────── + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + enabled: root.xmuxEnabled + + // maxConcurrency + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("maxConcurrency") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.maxConcurrencyMin + maxValue: root.maxConcurrencyMax + onMinChanged: root.maxConcurrencyMin = val + onMaxChanged: root.maxConcurrencyMax = val + } + + // maxConnections + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("maxConnections") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.maxConnectionsMin + maxValue: root.maxConnectionsMax + onMinChanged: root.maxConnectionsMin = val + onMaxChanged: root.maxConnectionsMax = val + } + + // cMaxReuseTimes + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("cMaxReuseTimes") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.cMaxReuseTimesMin + maxValue: root.cMaxReuseTimesMax + onMinChanged: root.cMaxReuseTimesMin = val + onMaxChanged: root.cMaxReuseTimesMax = val + } + + // hMaxRequestTimes + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("hMaxRequestTimes") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.hMaxRequestTimesMin + maxValue: root.hMaxRequestTimesMax + onMinChanged: root.hMaxRequestTimesMin = val + onMaxChanged: root.hMaxRequestTimesMax = val + } + + // hMaxReusableSecs + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("hMaxReusableSecs") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.hMaxReusableSecsMin + maxValue: root.hMaxReusableSecsMax + onMinChanged: root.hMaxReusableSecsMin = val + onMaxChanged: root.hMaxReusableSecsMax = val + } + + // hKeepAlivePeriod — single field + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + headerText: qsTr("hKeepAlivePeriod") + textField.text: root.hKeepAlivePeriod + textField.validator: IntValidator { + bottom: 0 + } + textField.onEditingFinished: root.hKeepAlivePeriod = textField.text + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + // ── Save button ─────────────────────────────────────────────────── + BasicButtonType { + id: saveButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + text: qsTr("Save") + onClicked: { + forceActiveFocus() + // XrayConfigModel.setXmux(...) + } + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } +} diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index f2a462630..1314d3324 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -77,6 +77,12 @@ Pages2/PageProtocolRaw.qml Pages2/PageProtocolWireGuardSettings.qml Pages2/PageProtocolXraySettings.qml + + Pages2/PageProtocolXrayTransportSettings.qml + Pages2/PageProtocolXrayXmuxSettings.qml + Pages2/PageProtocolXrayXPaddingSettings.qml + Controls2/MinMaxRowType.qml + Pages2/PageServiceDnsSettings.qml Pages2/PageServiceSftpSettings.qml Pages2/PageServiceSocksProxySettings.qml From dc58e1832c03c2db288ceb070f31735a79a8fef2 Mon Sep 17 00:00:00 2001 From: dranik Date: Tue, 24 Mar 2026 11:34:03 +0300 Subject: [PATCH 02/18] add UI PageProtocolXrayConfigsSettings, PageProtocolXrayFlowSettings, PageProtocolXraySecuritySettings --- client/ui/controllers/qml/pageController.h | 3 + .../PageProtocolXrayConfigsSettings.qml | 217 ++++++++++++ .../Pages2/PageProtocolXrayFlowSettings.qml | 109 ++++++ .../PageProtocolXraySecuritySettings.qml | 316 ++++++++++++++++++ .../qml/Pages2/PageProtocolXraySettings.qml | 3 + .../PageProtocolXrayTransportSettings.qml | 73 ++-- client/ui/qml/qml.qrc | 3 + 7 files changed, 702 insertions(+), 22 deletions(-) create mode 100644 client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml create mode 100644 client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 57362aa6a..4fc298d81 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -82,9 +82,12 @@ namespace PageLoader PageDevMenu, + PageProtocolXrayConfigsSettings, PageProtocolXrayTransportSettings, PageProtocolXrayXmuxSettings, PageProtocolXrayXPaddingSettings, + PageProtocolXrayFlowSettings, + PageProtocolXraySecuritySettings, }; Q_ENUM_NS(PageEnum) diff --git a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml new file mode 100644 index 000000000..ef3e11c32 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml @@ -0,0 +1,217 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + // Temporary model — will be replaced by real XrayConfigsModel + ListModel { + id: configsModel + ListElement { + configName: "XHTTP TLS Reality"; configDate: "24.02.2026 11:12" + } + ListElement { + configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14" + } + ListElement { + configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14" + } + ListElement { + configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:15" + } + } + + // Currently selected config for the drawer + property string selectedConfigName: "" + property int selectedConfigIndex: -1 + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + header: ColumnLayout { + width: listView.width + spacing: 0 + + // ── Header ──────────────────────────────────────────────── + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + text: qsTr("XRay Configurations") + wrapMode: Text.WordWrap + } + + // ── Create config from current settings ─────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Create configuration based on current settings") + textMaximumLineCount: 2 + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + // XrayConfigModel.createConfigFromCurrent() + } + } + + DividerType { + } + + // ── Export ──────────────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Export settings") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + // XrayConfigModel.exportSettings() + } + } + + DividerType { + } + + // ── Import ──────────────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Import settings") + descriptionText: qsTr("In JSON format") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + // XrayConfigModel.importSettings() + } + } + + DividerType { + } + + // ── Configurations section label ────────────────────────── + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 24 + Layout.bottomMargin: 8 + text: qsTr("Configurations") + color: AmneziaStyle.color.mutedGray + } + } + + model: configsModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + LabelWithButtonType { + Layout.fillWidth: true + + text: configName + descriptionText: configDate + + rightImageSource: "qrc:/images/controls/more-vertical.svg" + + clickedFunction: function () { + root.selectedConfigName = configName + root.selectedConfigIndex = index + configActionsDrawer.openTriggered() + } + } + + DividerType { + } + } + } + + // ── Per-config actions drawer ───────────────────────────────────── + DrawerType2 { + id: configActionsDrawer + + parent: root + anchors.fill: parent + expandedHeight: root.height * 0.4 + + expandedStateContent: ColumnLayout { + id: drawerContent + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + onImplicitHeightChanged: { + configActionsDrawer.expandedHeight = drawerContent.implicitHeight + 32 + } + + BackButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + backButtonFunction: function () { + configActionsDrawer.closeTriggered() + } + } + + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 16 + text: root.selectedConfigName + wrapMode: Text.WordWrap + } + + // ── Apply config ────────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Apply configuration") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function () { + configActionsDrawer.closeTriggered() + // XrayConfigModel.applyConfig(root.selectedConfigIndex) + } + } + + DividerType { + } + + // ── Delete config ───────────────────────────────────────── + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Delete configuration") + textColor: AmneziaStyle.color.vibrantRed + clickedFunction: function () { + configActionsDrawer.closeTriggered() + // XrayConfigModel.deleteConfig(root.selectedConfigIndex) + } + } + + DividerType { + } + + Item { + Layout.preferredHeight: 16 + } + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml new file mode 100644 index 000000000..1309b5315 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml @@ -0,0 +1,109 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + // Temporary local state — will be replaced by model role + property int selectedFlow: 1 // 0=Empty, 1=xtls-rprx-vision, 2=xtls-rprx-vision-udp443 + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + FlickableType { + id: flickable + anchors.top: backButton.bottom + anchors.bottom: saveButton.top + anchors.left: parent.left + anchors.right: parent.right + contentHeight: mainColumn.implicitHeight + + ColumnLayout { + id: mainColumn + width: flickable.width + spacing: 0 + + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + text: qsTr("Flow") + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("Empty") + checked: root.selectedFlow === 0 + onClicked: root.selectedFlow = 0 + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("xtls-rprx-vision") + checked: root.selectedFlow === 1 + onClicked: root.selectedFlow = 1 + } + + DividerType { + } + + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("xtls-rprx-vision-udp443") + checked: root.selectedFlow === 2 + onClicked: root.selectedFlow = 2 + } + + DividerType { + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + text: qsTr("Save") + onClicked: { + forceActiveFocus() + // XrayConfigModel.setFlow(...) + } + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml new file mode 100644 index 000000000..377e359a8 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -0,0 +1,316 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + // Temporary local state — will be replaced by model roles + property int selectedSecurity: 2 // 0=None, 1=TLS, 2=Reality + + // Shared TLS + Reality fields + property string fingerprint: "Mozilla/5.0" + property string serverName: "cdn.example.com" + + // TLS-only fields + property string alpn: "HTTP/2" + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + FlickableType { + id: flickable + anchors.top: backButton.bottom + anchors.bottom: saveButton.top + anchors.left: parent.left + anchors.right: parent.right + contentHeight: mainColumn.implicitHeight + + ColumnLayout { + id: mainColumn + width: flickable.width + spacing: 0 + + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + text: qsTr("Security") + } + + // ── Radio: None ─────────────────────────────────────────── + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("None") + checked: root.selectedSecurity === 0 + onClicked: root.selectedSecurity = 0 + } + + DividerType { + } + + // ── Radio: TLS ──────────────────────────────────────────── + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("TLS") + checked: root.selectedSecurity === 1 + onClicked: root.selectedSecurity = 1 + } + + DividerType { + } + + // ── Radio: Reality ──────────────────────────────────────── + VerticalRadioButton { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("Reality") + checked: root.selectedSecurity === 2 + onClicked: root.selectedSecurity = 2 + } + + DividerType { + } + + // ══════════════════════════════════════════════════════════ + // TLS fields (ALPN + Fingerprint + SNI) + // ══════════════════════════════════════════════════════════ + ColumnLayout { + visible: root.selectedSecurity === 1 + Layout.fillWidth: true + spacing: 0 + + DropDownType { + id: tlsAlpnDropDown + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.alpn + descriptionText: qsTr("ALPN") + headerText: qsTr("ALPN") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "HTTP/2" + } + ListElement { + name: "HTTP/1.1" + } + ListElement { + name: "HTTP/2,HTTP/1.1" + } + } + clickedFunction: function () { + root.alpn = selectedText + tlsAlpnDropDown.text = selectedText + tlsAlpnDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.alpn) { + selectedIndex = i; + break + } + } + } + } + } + + DropDownType { + id: tlsFingerprintDropDown + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.fingerprint + descriptionText: qsTr("Fingerprint") + headerText: qsTr("Fingerprint") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Mozilla/5.0" + } + ListElement { + name: "Chrome" + } + ListElement { + name: "Firefox" + } + ListElement { + name: "Safari" + } + ListElement { + name: "iOS" + } + ListElement { + name: "Android" + } + ListElement { + name: "Edge" + } + ListElement { + name: "360" + } + ListElement { + name: "QQ" + } + ListElement { + name: "Random" + } + } + clickedFunction: function () { + root.fingerprint = selectedText + tlsFingerprintDropDown.text = selectedText + tlsFingerprintDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.fingerprint) { + selectedIndex = i; + break + } + } + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Server Name (SNI)") + textField.text: root.serverName + textField.onEditingFinished: root.serverName = textField.text + } + } + + // ══════════════════════════════════════════════════════════ + // Reality fields (Fingerprint + SNI) + // ══════════════════════════════════════════════════════════ + ColumnLayout { + visible: root.selectedSecurity === 2 + Layout.fillWidth: true + spacing: 0 + + DropDownType { + id: realityFingerprintDropDown + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: root.fingerprint + descriptionText: qsTr("Fingerprint") + headerText: qsTr("Fingerprint") + drawerParent: root + listView: ListViewWithRadioButtonType { + rootWidth: root.width + model: ListModel { + ListElement { + name: "Mozilla/5.0" + } + ListElement { + name: "Chrome" + } + ListElement { + name: "Firefox" + } + ListElement { + name: "Safari" + } + ListElement { + name: "iOS" + } + ListElement { + name: "Android" + } + ListElement { + name: "Edge" + } + ListElement { + name: "360" + } + ListElement { + name: "QQ" + } + ListElement { + name: "Random" + } + } + clickedFunction: function () { + root.fingerprint = selectedText + realityFingerprintDropDown.text = selectedText + realityFingerprintDropDown.closeTriggered() + } + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + if (model.get(i).name === root.fingerprint) { + selectedIndex = i; + break + } + } + } + } + } + + TextFieldWithHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + headerText: qsTr("Server Name (SNI)") + textField.text: root.serverName + textField.onEditingFinished: root.serverName = textField.text + } + } + + Item { + Layout.preferredHeight: 16 + } + } + } + + BasicButtonType { + id: saveButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + text: qsTr("Save") + onClicked: { + forceActiveFocus() + // XrayConfigModel.setSecurity(...) + } + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index dd9455c9c..9739c1317 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -69,6 +69,9 @@ PageType { implicitHeight: 40 image: "qrc:/images/controls/more-vertical.svg" imageColor: AmneziaStyle.color.mutedGray + onClicked: function () { + PageController.goToPage(PageEnum.PageProtocolXrayConfigsSettings) + } } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml index 093990fdb..c29136f4d 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -35,6 +35,12 @@ PageType { // Traffic Shaping property string uplinkChunkSize: "0" property string scMaxBufferedPosts: "" + property string scMaxEachPostBytesMin: "1" + property string scMaxEachPostBytesMax: "100" + property string scMinPostsIntervalMsMin: "100" + property string scMinPostsIntervalMsMax: "800" + property string scStreamUpServerSecsMin: "1" + property string scStreamUpServerSecsMax: "100" // mKCP fields property string mkcpTti: "" property string mkcpUplinkCapacity: "" @@ -603,41 +609,64 @@ PageType { textField.onEditingFinished: root.scMaxBufferedPosts = textField.text } - // scMaxEachPostBytes → nav row - LabelWithButtonType { + // scMaxEachPostBytes — min/max range + CaptionTextType { Layout.fillWidth: true - Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 text: qsTr("scMaxEachPostBytes") - descriptionText: qsTr("1—100") - rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function () { /* navigate */ - } + color: AmneziaStyle.color.mutedGray } - DividerType { + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.scMaxEachPostBytesMin + maxValue: root.scMaxEachPostBytesMax + onMinChanged: root.scMaxEachPostBytesMin = val + onMaxChanged: root.scMaxEachPostBytesMax = val } - // scMinPostsIntervalMs → nav row - LabelWithButtonType { + // scMinPostsIntervalMs — min/max range + CaptionTextType { Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 text: qsTr("scMinPostsIntervalMs") - descriptionText: qsTr("100—600ms") - rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function () { /* navigate */ - } + color: AmneziaStyle.color.mutedGray } - DividerType { + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.scMinPostsIntervalMsMin + maxValue: root.scMinPostsIntervalMsMax + onMinChanged: root.scMinPostsIntervalMsMin = val + onMaxChanged: root.scMinPostsIntervalMsMax = val } - // scStreamUpServerSecs → nav row - LabelWithButtonType { + // scStreamUpServerSecs — min/max range + CaptionTextType { Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 text: qsTr("scStreamUpServerSecs") - descriptionText: qsTr("1—100") - rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function () { /* navigate */ - } + color: AmneziaStyle.color.mutedGray } - DividerType { + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: root.scStreamUpServerSecsMin + maxValue: root.scStreamUpServerSecsMax + onMinChanged: root.scStreamUpServerSecsMin = val + onMaxChanged: root.scStreamUpServerSecsMax = val } // ── Padding and multiplexing ────────────────────────── diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index 1314d3324..d70a531b6 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -78,6 +78,9 @@ Pages2/PageProtocolWireGuardSettings.qml Pages2/PageProtocolXraySettings.qml + Pages2/PageProtocolXrayConfigsSettings.qml + Pages2/PageProtocolXrayFlowSettings.qml + Pages2/PageProtocolXraySecuritySettings.qml Pages2/PageProtocolXrayTransportSettings.qml Pages2/PageProtocolXrayXmuxSettings.qml Pages2/PageProtocolXrayXPaddingSettings.qml From a028349dbb92b83c3e888b8d9f98b19517114c7e Mon Sep 17 00:00:00 2001 From: dranik Date: Tue, 24 Mar 2026 11:54:51 +0300 Subject: [PATCH 03/18] add Xray-specific keys --- .../models/protocols/xrayProtocolConfig.cpp | 343 ++++++++++++++---- .../models/protocols/xrayProtocolConfig.h | 123 ++++++- client/core/utils/constants/configKeys.h | 70 ++++ 3 files changed, 443 insertions(+), 93 deletions(-) diff --git a/client/core/models/protocols/xrayProtocolConfig.cpp b/client/core/models/protocols/xrayProtocolConfig.cpp index bb4e61457..be3edee3b 100644 --- a/client/core/models/protocols/xrayProtocolConfig.cpp +++ b/client/core/models/protocols/xrayProtocolConfig.cpp @@ -3,86 +3,274 @@ #include #include -#include "../../../core/utils/protocolEnum.h" -#include "../../../core/protocols/protocolUtils.h" -#include "../../../core/utils/constants/configKeys.h" -#include "../../../core/utils/constants/protocolConstants.h" +#include "core/utils/protocolEnum.h" +#include "core/protocols/protocolUtils.h" +#include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" using namespace amnezia; using namespace ProtocolUtils; + namespace amnezia { +// ═════════════════════════════════════════════════════════════════════════════ +// XrayXPaddingConfig +// ═════════════════════════════════════════════════════════════════════════════ + +QJsonObject XrayXPaddingConfig::toJson() const +{ + QJsonObject obj; + if (!bytesMin.isEmpty()) obj[configKey::xPaddingBytesMin] = bytesMin; + if (!bytesMax.isEmpty()) obj[configKey::xPaddingBytesMax] = bytesMax; + obj[configKey::xPaddingObfsMode] = obfsMode; + if (!key.isEmpty()) obj[configKey::xPaddingKey] = key; + if (!header.isEmpty()) obj[configKey::xPaddingHeader] = header; + if (!placement.isEmpty()) obj[configKey::xPaddingPlacement] = placement; + if (!method.isEmpty()) obj[configKey::xPaddingMethod] = method; + return obj; +} + +XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json) +{ + XrayXPaddingConfig c; + c.bytesMin = json.value(configKey::xPaddingBytesMin).toString(); + c.bytesMax = json.value(configKey::xPaddingBytesMax).toString(); + c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true); + c.key = json.value(configKey::xPaddingKey).toString("www.googletagmanager.com"); + c.header = json.value(configKey::xPaddingHeader).toString(); + c.placement = json.value(configKey::xPaddingPlacement).toString("Cookie"); + c.method = json.value(configKey::xPaddingMethod).toString("Repeat-x"); + return c; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// XrayXmuxConfig +// ═════════════════════════════════════════════════════════════════════════════ + +QJsonObject XrayXmuxConfig::toJson() const +{ + QJsonObject obj; + obj[configKey::xmuxEnabled] = enabled; + if (!maxConcurrencyMin.isEmpty()) obj[configKey::xmuxMaxConcurrencyMin] = maxConcurrencyMin; + if (!maxConcurrencyMax.isEmpty()) obj[configKey::xmuxMaxConcurrencyMax] = maxConcurrencyMax; + if (!maxConnectionsMin.isEmpty()) obj[configKey::xmuxMaxConnectionsMin] = maxConnectionsMin; + if (!maxConnectionsMax.isEmpty()) obj[configKey::xmuxMaxConnectionsMax] = maxConnectionsMax; + if (!cMaxReuseTimesMin.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMin] = cMaxReuseTimesMin; + if (!cMaxReuseTimesMax.isEmpty()) obj[configKey::xmuxCMaxReuseTimesMax] = cMaxReuseTimesMax; + if (!hMaxRequestTimesMin.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMin] = hMaxRequestTimesMin; + if (!hMaxRequestTimesMax.isEmpty()) obj[configKey::xmuxHMaxRequestTimesMax] = hMaxRequestTimesMax; + if (!hMaxReusableSecsMin.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMin] = hMaxReusableSecsMin; + if (!hMaxReusableSecsMax.isEmpty()) obj[configKey::xmuxHMaxReusableSecsMax] = hMaxReusableSecsMax; + if (!hKeepAlivePeriod.isEmpty()) obj[configKey::xmuxHKeepAlivePeriod] = hKeepAlivePeriod; + return obj; +} + +XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json) +{ + XrayXmuxConfig c; + c.enabled = json.value(configKey::xmuxEnabled).toBool(true); + c.maxConcurrencyMin = json.value(configKey::xmuxMaxConcurrencyMin).toString("0"); + c.maxConcurrencyMax = json.value(configKey::xmuxMaxConcurrencyMax).toString("0"); + c.maxConnectionsMin = json.value(configKey::xmuxMaxConnectionsMin).toString("0"); + c.maxConnectionsMax = json.value(configKey::xmuxMaxConnectionsMax).toString("0"); + c.cMaxReuseTimesMin = json.value(configKey::xmuxCMaxReuseTimesMin).toString("0"); + c.cMaxReuseTimesMax = json.value(configKey::xmuxCMaxReuseTimesMax).toString("0"); + c.hMaxRequestTimesMin = json.value(configKey::xmuxHMaxRequestTimesMin).toString("0"); + c.hMaxRequestTimesMax = json.value(configKey::xmuxHMaxRequestTimesMax).toString("0"); + c.hMaxReusableSecsMin = json.value(configKey::xmuxHMaxReusableSecsMin).toString("0"); + c.hMaxReusableSecsMax = json.value(configKey::xmuxHMaxReusableSecsMax).toString("0"); + c.hKeepAlivePeriod = json.value(configKey::xmuxHKeepAlivePeriod).toString(); + return c; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// XrayXhttpConfig +// ═════════════════════════════════════════════════════════════════════════════ + +QJsonObject XrayXhttpConfig::toJson() const +{ + QJsonObject obj; + if (!mode.isEmpty()) obj[configKey::xhttpMode] = mode; + if (!host.isEmpty()) obj[configKey::xhttpHost] = host; + if (!path.isEmpty()) obj[configKey::xhttpPath] = path; + if (!headersTemplate.isEmpty()) obj[configKey::xhttpHeadersTemplate] = headersTemplate; + if (!uplinkMethod.isEmpty()) obj[configKey::xhttpUplinkMethod] = uplinkMethod; + obj[configKey::xhttpDisableGrpc] = disableGrpc; + obj[configKey::xhttpDisableSse] = disableSse; + + if (!sessionPlacement.isEmpty()) obj[configKey::xhttpSessionPlacement] = sessionPlacement; + if (!sessionKey.isEmpty()) obj[configKey::xhttpSessionKey] = sessionKey; + if (!seqPlacement.isEmpty()) obj[configKey::xhttpSeqPlacement] = seqPlacement; + if (!seqKey.isEmpty()) obj[configKey::xhttpSeqKey] = seqKey; + if (!uplinkDataPlacement.isEmpty()) obj[configKey::xhttpUplinkDataPlacement] = uplinkDataPlacement; + if (!uplinkDataKey.isEmpty()) obj[configKey::xhttpUplinkDataKey] = uplinkDataKey; + + if (!uplinkChunkSize.isEmpty()) obj[configKey::xhttpUplinkChunkSize] = uplinkChunkSize; + if (!scMaxBufferedPosts.isEmpty()) obj[configKey::xhttpScMaxBufferedPosts] = scMaxBufferedPosts; + if (!scMaxEachPostBytesMin.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMin] = scMaxEachPostBytesMin; + if (!scMaxEachPostBytesMax.isEmpty()) obj[configKey::xhttpScMaxEachPostBytesMax] = scMaxEachPostBytesMax; + if (!scMinPostsIntervalMsMin.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMin] = scMinPostsIntervalMsMin; + if (!scMinPostsIntervalMsMax.isEmpty()) obj[configKey::xhttpScMinPostsIntervalMsMax] = scMinPostsIntervalMsMax; + if (!scStreamUpServerSecsMin.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMin] = scStreamUpServerSecsMin; + if (!scStreamUpServerSecsMax.isEmpty()) obj[configKey::xhttpScStreamUpServerSecsMax] = scStreamUpServerSecsMax; + + obj["xPadding"] = xPadding.toJson(); + obj["xmux"] = xmux.toJson(); + + return obj; +} + +XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json) +{ + XrayXhttpConfig c; + c.mode = json.value(configKey::xhttpMode).toString("Auto"); + c.host = json.value(configKey::xhttpHost).toString("www.googletagmanager.com"); + c.path = json.value(configKey::xhttpPath).toString(); + c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString("HTTP"); + c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString("POST"); + c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true); + c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true); + + c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString("Path"); + c.sessionKey = json.value(configKey::xhttpSessionKey).toString("Path"); + c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString("Path"); + c.seqKey = json.value(configKey::xhttpSeqKey).toString(); + c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString("Body"); + c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString(); + + c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0"); + c.scMaxBufferedPosts = json.value(configKey::xhttpScMaxBufferedPosts).toString(); + c.scMaxEachPostBytesMin = json.value(configKey::xhttpScMaxEachPostBytesMin).toString("1"); + c.scMaxEachPostBytesMax = json.value(configKey::xhttpScMaxEachPostBytesMax).toString("100"); + c.scMinPostsIntervalMsMin = json.value(configKey::xhttpScMinPostsIntervalMsMin).toString("100"); + c.scMinPostsIntervalMsMax = json.value(configKey::xhttpScMinPostsIntervalMsMax).toString("800"); + c.scStreamUpServerSecsMin = json.value(configKey::xhttpScStreamUpServerSecsMin).toString("1"); + c.scStreamUpServerSecsMax = json.value(configKey::xhttpScStreamUpServerSecsMax).toString("100"); + + c.xPadding = XrayXPaddingConfig::fromJson(json.value("xPadding").toObject()); + c.xmux = XrayXmuxConfig::fromJson(json.value("xmux").toObject()); + + return c; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// XrayMkcpConfig +// ═════════════════════════════════════════════════════════════════════════════ + +QJsonObject XrayMkcpConfig::toJson() const +{ + QJsonObject obj; + if (!tti.isEmpty()) obj[configKey::mkcpTti] = tti; + if (!uplinkCapacity.isEmpty()) obj[configKey::mkcpUplinkCapacity] = uplinkCapacity; + if (!downlinkCapacity.isEmpty()) obj[configKey::mkcpDownlinkCapacity] = downlinkCapacity; + if (!readBufferSize.isEmpty()) obj[configKey::mkcpReadBufferSize] = readBufferSize; + if (!writeBufferSize.isEmpty()) obj[configKey::mkcpWriteBufferSize] = writeBufferSize; + obj[configKey::mkcpCongestion] = congestion; + return obj; +} + +XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json) +{ + XrayMkcpConfig c; + c.tti = json.value(configKey::mkcpTti).toString(); + c.uplinkCapacity = json.value(configKey::mkcpUplinkCapacity).toString(); + c.downlinkCapacity = json.value(configKey::mkcpDownlinkCapacity).toString(); + c.readBufferSize = json.value(configKey::mkcpReadBufferSize).toString(); + c.writeBufferSize = json.value(configKey::mkcpWriteBufferSize).toString(); + c.congestion = json.value(configKey::mkcpCongestion).toBool(true); + return c; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// XrayServerConfig +// ═════════════════════════════════════════════════════════════════════════════ + QJsonObject XrayServerConfig::toJson() const { QJsonObject obj; - - if (!port.isEmpty()) { - obj[configKey::port] = port; - } - if (!transportProto.isEmpty()) { - obj[configKey::transportProto] = transportProto; - } - if (!subnetAddress.isEmpty()) { - obj[configKey::subnetAddress] = subnetAddress; - } - if (!site.isEmpty()) { - obj[configKey::site] = site; - } - - if (isThirdPartyConfig) { - obj[configKey::isThirdPartyConfig] = isThirdPartyConfig; - } - + + // Existing fields + if (!port.isEmpty()) obj[configKey::port] = port; + if (!transportProto.isEmpty()) obj[configKey::transportProto] = transportProto; + if (!subnetAddress.isEmpty()) obj[configKey::subnetAddress] = subnetAddress; + if (!site.isEmpty()) obj[configKey::site] = site; + if (isThirdPartyConfig) obj[configKey::isThirdPartyConfig] = isThirdPartyConfig; + + // New: Security + if (!security.isEmpty()) obj[configKey::xraySecurity] = security; + if (!flow.isEmpty()) obj[configKey::xrayFlow] = flow; + if (!fingerprint.isEmpty()) obj[configKey::xrayFingerprint] = fingerprint; + if (!sni.isEmpty()) obj[configKey::xraySni] = sni; + if (!alpn.isEmpty()) obj[configKey::xrayAlpn] = alpn; + + // New: Transport + if (!transport.isEmpty()) obj[configKey::xrayTransport] = transport; + obj["xhttp"] = xhttp.toJson(); + obj["mkcp"] = mkcp.toJson(); + return obj; } -XrayServerConfig XrayServerConfig::fromJson(const QJsonObject& json) +XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json) { - XrayServerConfig config; - - config.port = json.value(configKey::port).toString(); - config.transportProto = json.value(configKey::transportProto).toString(); - config.subnetAddress = json.value(configKey::subnetAddress).toString(); - config.site = json.value(configKey::site).toString(); - - config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); - - return config; + XrayServerConfig c; + + // Existing fields + c.port = json.value(configKey::port).toString(); + c.transportProto = json.value(configKey::transportProto).toString(); + c.subnetAddress = json.value(configKey::subnetAddress).toString(); + c.site = json.value(configKey::site).toString(); + c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); + + // New: Security + c.security = json.value(configKey::xraySecurity).toString("reality"); + c.flow = json.value(configKey::xrayFlow).toString("xtls-rprx-vision"); + c.fingerprint = json.value(configKey::xrayFingerprint).toString("Mozilla/5.0"); + c.sni = json.value(configKey::xraySni).toString("cdn.example.com"); + c.alpn = json.value(configKey::xrayAlpn).toString("HTTP/2"); + + // New: Transport + c.transport = json.value(configKey::xrayTransport).toString("raw"); + c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject()); + c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject()); + + return c; } -bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig& other) const +bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const { - return port == other.port && site == other.site; + return port == other.port + && site == other.site + && security == other.security + && flow == other.flow + && transport == other.transport + && fingerprint == other.fingerprint + && sni == other.sni; } +// ═════════════════════════════════════════════════════════════════════════════ +// XrayClientConfig (unchanged logic, kept as-is) +// ═════════════════════════════════════════════════════════════════════════════ + QJsonObject XrayClientConfig::toJson() const { QJsonObject obj; - - if (!nativeConfig.isEmpty()) { - obj[configKey::config] = nativeConfig; - } - if (!localPort.isEmpty()) { - obj[configKey::localPort] = localPort; - } - if (!id.isEmpty()) { - obj[configKey::clientId] = id; - } - + if (!nativeConfig.isEmpty()) obj[configKey::config] = nativeConfig; + if (!localPort.isEmpty()) obj[configKey::localPort] = localPort; + if (!id.isEmpty()) obj[configKey::clientId] = id; return obj; } -XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json) +XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json) { - XrayClientConfig config; - - config.nativeConfig = json.value(configKey::config).toString(); - config.localPort = json.value(configKey::localPort).toString(); - config.id = json.value(configKey::clientId).toString(); - - if (config.id.isEmpty() && !config.nativeConfig.isEmpty()) { - QJsonDocument doc = QJsonDocument::fromJson(config.nativeConfig.toUtf8()); + XrayClientConfig c; + c.nativeConfig = json.value(configKey::config).toString(); + c.localPort = json.value(configKey::localPort).toString(); + c.id = json.value(configKey::clientId).toString(); + + if (c.id.isEmpty() && !c.nativeConfig.isEmpty()) { + QJsonDocument doc = QJsonDocument::fromJson(c.nativeConfig.toUtf8()); if (!doc.isNull() && doc.isObject()) { QJsonObject configObj = doc.object(); if (configObj.contains(protocols::xray::outbounds)) { @@ -98,10 +286,7 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json) if (vnextObj.contains(protocols::xray::users)) { QJsonArray users = vnextObj[protocols::xray::users].toArray(); if (!users.isEmpty()) { - QJsonObject user = users[0].toObject(); - if (user.contains(protocols::xray::id)) { - config.id = user[protocols::xray::id].toString(); - } + c.id = users[0].toObject().value(protocols::xray::id).toString(); } } } @@ -111,16 +296,19 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject& json) } } } - - return config; + + return c; } +// ═════════════════════════════════════════════════════════════════════════════ +// XrayProtocolConfig (unchanged logic, kept as-is) +// ═════════════════════════════════════════════════════════════════════════════ + QJsonObject XrayProtocolConfig::toJson() const { QJsonObject obj = serverConfig.toJson(); - + if (clientConfig.has_value()) { - // Third-party import: nativeConfig is raw Xray JSON (inbounds/outbounds) QJsonDocument doc = QJsonDocument::fromJson(clientConfig->nativeConfig.toUtf8()); if (!doc.isNull() && doc.isObject() && doc.object().contains(protocols::xray::outbounds) && !doc.object().contains(configKey::config)) { @@ -130,42 +318,38 @@ QJsonObject XrayProtocolConfig::toJson() const obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact)); } } - + return obj; } -XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject& json) +XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json) { - XrayProtocolConfig config; - - config.serverConfig = XrayServerConfig::fromJson(json); - + XrayProtocolConfig c; + c.serverConfig = XrayServerConfig::fromJson(json); + QString lastConfigStr = json.value(configKey::lastConfig).toString(); if (!lastConfigStr.isEmpty()) { QJsonDocument doc = QJsonDocument::fromJson(lastConfigStr.toUtf8()); if (doc.isObject()) { QJsonObject parsed = doc.object(); - // Third-party import stores raw Xray config (inbounds/outbounds) directly if (parsed.contains(protocols::xray::outbounds) && !parsed.contains(configKey::config)) { XrayClientConfig clientCfg; clientCfg.nativeConfig = lastConfigStr; if (parsed.contains(protocols::xray::inbounds)) { QJsonArray inbounds = parsed.value(protocols::xray::inbounds).toArray(); if (!inbounds.isEmpty()) { - QJsonObject inbound = inbounds[0].toObject(); - if (inbound.contains(protocols::xray::port)) { - clientCfg.localPort = QString::number(inbound.value(protocols::xray::port).toInt()); - } + clientCfg.localPort = QString::number( + inbounds[0].toObject().value(protocols::xray::port).toInt()); } } - config.clientConfig = clientCfg; + c.clientConfig = clientCfg; } else { - config.clientConfig = XrayClientConfig::fromJson(parsed); + c.clientConfig = XrayClientConfig::fromJson(parsed); } } } - - return config; + + return c; } bool XrayProtocolConfig::hasClientConfig() const @@ -173,7 +357,7 @@ bool XrayProtocolConfig::hasClientConfig() const return clientConfig.has_value(); } -void XrayProtocolConfig::setClientConfig(const XrayClientConfig& config) +void XrayProtocolConfig::setClientConfig(const XrayClientConfig &config) { clientConfig = config; } @@ -184,4 +368,3 @@ void XrayProtocolConfig::clearClientConfig() } } // namespace amnezia - diff --git a/client/core/models/protocols/xrayProtocolConfig.h b/client/core/models/protocols/xrayProtocolConfig.h index fc52a81b3..a4ae2645f 100644 --- a/client/core/models/protocols/xrayProtocolConfig.h +++ b/client/core/models/protocols/xrayProtocolConfig.h @@ -8,41 +8,138 @@ namespace amnezia { +// ── xPadding ───────────────────────────────────────────────────────────────── +struct XrayXPaddingConfig { + QString bytesMin; // xPaddingBytes min + QString bytesMax; // xPaddingBytes max + bool obfsMode = true; // xPaddingObfsMode + QString key; // xPaddingKey + QString header; // xPaddingHeader + QString placement = "Cookie"; // xPaddingPlacement: Cookie|Header|Query|Body + QString method = "Repeat-x"; // xPaddingMethod: Repeat-x|Random|Zero + + QJsonObject toJson() const; + static XrayXPaddingConfig fromJson(const QJsonObject &json); +}; + +// ── xmux ───────────────────────────────────────────────────────────────────── +struct XrayXmuxConfig { + bool enabled = true; + + QString maxConcurrencyMin = "0"; + QString maxConcurrencyMax = "0"; + QString maxConnectionsMin = "0"; + QString maxConnectionsMax = "0"; + QString cMaxReuseTimesMin = "0"; + QString cMaxReuseTimesMax = "0"; + QString hMaxRequestTimesMin = "0"; + QString hMaxRequestTimesMax = "0"; + QString hMaxReusableSecsMin = "0"; + QString hMaxReusableSecsMax = "0"; + QString hKeepAlivePeriod; + + QJsonObject toJson() const; + static XrayXmuxConfig fromJson(const QJsonObject &json); +}; + +// ── XHTTP transport ─────────────────────────────────────────────────────────── +struct XrayXhttpConfig { + QString mode = "Auto"; // Auto|Packet-up|Stream-up|Stream-one + QString host = "www.googletagmanager.com"; + QString path; + QString headersTemplate = "HTTP"; // HTTP|None + QString uplinkMethod = "POST"; // POST|PUT|PATCH + bool disableGrpc = true; + bool disableSse = true; + + // Session & Sequence + QString sessionPlacement = "Path"; // Path|Header|Cookie|None + QString sessionKey = "Path"; + QString seqPlacement = "Path"; + QString seqKey; + QString uplinkDataPlacement = "Body"; // Body|Query + QString uplinkDataKey; + + // Traffic Shaping + QString uplinkChunkSize = "0"; + QString scMaxBufferedPosts; + QString scMaxEachPostBytesMin = "1"; + QString scMaxEachPostBytesMax = "100"; + QString scMinPostsIntervalMsMin = "100"; + QString scMinPostsIntervalMsMax = "800"; + QString scStreamUpServerSecsMin = "1"; + QString scStreamUpServerSecsMax = "100"; + + XrayXPaddingConfig xPadding; + XrayXmuxConfig xmux; + + QJsonObject toJson() const; + static XrayXhttpConfig fromJson(const QJsonObject &json); +}; + +// ── mKCP transport ──────────────────────────────────────────────────────────── +struct XrayMkcpConfig { + QString tti; + QString uplinkCapacity; + QString downlinkCapacity; + QString readBufferSize; + QString writeBufferSize; + bool congestion = true; + + QJsonObject toJson() const; + static XrayMkcpConfig fromJson(const QJsonObject &json); +}; + +// ── Server config (settings editable by user) ───────────────────────────────── struct XrayServerConfig { + // Existing fields QString port; QString transportProto; QString subnetAddress; QString site; - bool isThirdPartyConfig = false; - + bool isThirdPartyConfig = false; + + // New: Security + QString security = "reality"; // none|tls|reality + QString flow = "xtls-rprx-vision"; // ""|xtls-rprx-vision|xtls-rprx-vision-udp443 + QString fingerprint = "Mozilla/5.0"; + QString sni = "cdn.example.com"; + QString alpn = "HTTP/2"; // TLS only: HTTP/2|HTTP/1.1|HTTP/2,HTTP/1.1 + + // New: Transport + QString transport = "raw"; // raw|xhttp|mkcp + XrayXhttpConfig xhttp; + XrayMkcpConfig mkcp; + QJsonObject toJson() const; - static XrayServerConfig fromJson(const QJsonObject& json); - - bool hasEqualServerSettings(const XrayServerConfig& other) const; + static XrayServerConfig fromJson(const QJsonObject &json); + + bool hasEqualServerSettings(const XrayServerConfig &other) const; }; +// ── Client config (generated, not edited by user) ───────────────────────────── struct XrayClientConfig { QString nativeConfig; QString localPort; QString id; - + QJsonObject toJson() const; - static XrayClientConfig fromJson(const QJsonObject& json); + static XrayClientConfig fromJson(const QJsonObject &json); }; +// ── Top-level protocol config ────────────────────────────────────────────────── struct XrayProtocolConfig { - XrayServerConfig serverConfig; + XrayServerConfig serverConfig; std::optional clientConfig; - + QJsonObject toJson() const; - static XrayProtocolConfig fromJson(const QJsonObject& json); - + static XrayProtocolConfig fromJson(const QJsonObject &json); + bool hasClientConfig() const; - void setClientConfig(const XrayClientConfig& config); + void setClientConfig(const XrayClientConfig &config); void clearClientConfig(); }; } // namespace amnezia #endif // XRAYPROTOCOLCONFIG_H - diff --git a/client/core/utils/constants/configKeys.h b/client/core/utils/constants/configKeys.h index 40bc842b1..c6a7818c7 100644 --- a/client/core/utils/constants/configKeys.h +++ b/client/core/utils/constants/configKeys.h @@ -121,6 +121,76 @@ namespace amnezia constexpr QLatin1String latestHandshake("latestHandshake"); constexpr QLatin1String dataReceived("dataReceived"); constexpr QLatin1String dataSent("dataSent"); + + // ── Xray-specific keys ──────────────────────────────────────── + + // Security + constexpr QLatin1String xraySecurity("xray_security"); // none | tls | reality + constexpr QLatin1String xrayFlow("xray_flow"); // "" | xtls-rprx-vision | xtls-rprx-vision-udp443 + constexpr QLatin1String xrayFingerprint("xray_fingerprint"); // Mozilla/5.0 | chrome | firefox | ... + constexpr QLatin1String xraySni("xray_sni"); // Server Name (SNI) + constexpr QLatin1String xrayAlpn("xray_alpn"); // HTTP/2 | HTTP/1.1 | HTTP/2,HTTP/1.1 + + // Transport — common + constexpr QLatin1String xrayTransport("xray_transport"); // raw | xhttp | mkcp + + // Transport — XHTTP + constexpr QLatin1String xhttpMode("xhttp_mode"); // Auto | Packet-up | Stream-up | Stream-one + constexpr QLatin1String xhttpHost("xhttp_host"); + constexpr QLatin1String xhttpPath("xhttp_path"); + constexpr QLatin1String xhttpHeadersTemplate("xhttp_headers_template"); // HTTP | None + constexpr QLatin1String xhttpUplinkMethod("xhttp_uplink_method"); // POST | PUT | PATCH + constexpr QLatin1String xhttpDisableGrpc("xhttp_disable_grpc"); // bool + constexpr QLatin1String xhttpDisableSse("xhttp_disable_sse"); // bool + + // Transport — XHTTP Session & Sequence + constexpr QLatin1String xhttpSessionPlacement("xhttp_session_placement"); // Path | Header | Cookie | None + constexpr QLatin1String xhttpSessionKey("xhttp_session_key"); + constexpr QLatin1String xhttpSeqPlacement("xhttp_seq_placement"); + constexpr QLatin1String xhttpSeqKey("xhttp_seq_key"); + constexpr QLatin1String xhttpUplinkDataPlacement("xhttp_uplink_data_placement"); // Body | Query + constexpr QLatin1String xhttpUplinkDataKey("xhttp_uplink_data_key"); + + // Transport — XHTTP Traffic Shaping + constexpr QLatin1String xhttpUplinkChunkSize("xhttp_uplink_chunk_size"); + constexpr QLatin1String xhttpScMaxBufferedPosts("xhttp_sc_max_buffered_posts"); + constexpr QLatin1String xhttpScMaxEachPostBytesMin("xhttp_sc_max_each_post_bytes_min"); + constexpr QLatin1String xhttpScMaxEachPostBytesMax("xhttp_sc_max_each_post_bytes_max"); + constexpr QLatin1String xhttpScMinPostsIntervalMsMin("xhttp_sc_min_posts_interval_ms_min"); + constexpr QLatin1String xhttpScMinPostsIntervalMsMax("xhttp_sc_min_posts_interval_ms_max"); + constexpr QLatin1String xhttpScStreamUpServerSecsMin("xhttp_sc_stream_up_server_secs_min"); + constexpr QLatin1String xhttpScStreamUpServerSecsMax("xhttp_sc_stream_up_server_secs_max"); + + // Transport — mKCP + constexpr QLatin1String mkcpTti("mkcp_tti"); + constexpr QLatin1String mkcpUplinkCapacity("mkcp_uplink_capacity"); + constexpr QLatin1String mkcpDownlinkCapacity("mkcp_downlink_capacity"); + constexpr QLatin1String mkcpReadBufferSize("mkcp_read_buffer_size"); + constexpr QLatin1String mkcpWriteBufferSize("mkcp_write_buffer_size"); + constexpr QLatin1String mkcpCongestion("mkcp_congestion"); // bool + + // xPadding + constexpr QLatin1String xPaddingBytesMin("xpadding_bytes_min"); + constexpr QLatin1String xPaddingBytesMax("xpadding_bytes_max"); + constexpr QLatin1String xPaddingObfsMode("xpadding_obfs_mode"); // bool + constexpr QLatin1String xPaddingKey("xpadding_key"); + constexpr QLatin1String xPaddingHeader("xpadding_header"); + constexpr QLatin1String xPaddingPlacement("xpadding_placement"); // Cookie | Header | Query | Body + constexpr QLatin1String xPaddingMethod("xpadding_method"); // Repeat-x | Random | Zero + + // xmux + constexpr QLatin1String xmuxEnabled("xmux_enabled"); // bool + constexpr QLatin1String xmuxMaxConcurrencyMin("xmux_max_concurrency_min"); + constexpr QLatin1String xmuxMaxConcurrencyMax("xmux_max_concurrency_max"); + constexpr QLatin1String xmuxMaxConnectionsMin("xmux_max_connections_min"); + constexpr QLatin1String xmuxMaxConnectionsMax("xmux_max_connections_max"); + constexpr QLatin1String xmuxCMaxReuseTimesMin("xmux_c_max_reuse_times_min"); + constexpr QLatin1String xmuxCMaxReuseTimesMax("xmux_c_max_reuse_times_max"); + constexpr QLatin1String xmuxHMaxRequestTimesMin("xmux_h_max_request_times_min"); + constexpr QLatin1String xmuxHMaxRequestTimesMax("xmux_h_max_request_times_max"); + constexpr QLatin1String xmuxHMaxReusableSecsMin("xmux_h_max_reusable_secs_min"); + constexpr QLatin1String xmuxHMaxReusableSecsMax("xmux_h_max_reusable_secs_max"); + constexpr QLatin1String xmuxHKeepAlivePeriod("xmux_h_keep_alive_period"); } } From b91ea953030ca28d9cd8c3bb367105d6bea39afd Mon Sep 17 00:00:00 2001 From: dranik Date: Wed, 25 Mar 2026 07:57:54 +0300 Subject: [PATCH 04/18] add vars xray model --- .../ui/models/protocols/xrayConfigModel.cpp | 341 ++++++++++++++++-- client/ui/models/protocols/xrayConfigModel.h | 87 ++++- 2 files changed, 388 insertions(+), 40 deletions(-) diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index 462982073..989a691ac 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -8,94 +8,373 @@ using namespace amnezia; using namespace ProtocolUtils; -XrayConfigModel::XrayConfigModel(QObject *parent) : QAbstractListModel(parent) +XrayConfigModel::XrayConfigModel(QObject* parent) : QAbstractListModel(parent) { } -int XrayConfigModel::rowCount(const QModelIndex &parent) const +int XrayConfigModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return 1; } -bool XrayConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) +bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) { + if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) + { return false; } - QString strValue = value.toString(); + auto& srv = m_protocolConfig.serverConfig; + auto& xhttp = srv.xhttp; + auto& mkcp = srv.mkcp; + auto& pad = xhttp.xPadding; + auto& mux = xhttp.xmux; + + QString str = value.toString(); + + switch (role) + { + // ── Main ────────────────────────────────────────────────────────── + case Roles::SiteRole: srv.site = str; + break; + case Roles::PortRole: srv.port = str; + break; + case Roles::TransportRole: srv.transport = str; + break; + case Roles::SecurityRole: srv.security = str; + break; + case Roles::FlowRole: srv.flow = str; + break; + + // ── Security ────────────────────────────────────────────────────── + case Roles::FingerprintRole: srv.fingerprint = str; + break; + case Roles::SniRole: srv.sni = str; + break; + case Roles::AlpnRole: srv.alpn = str; + break; + + // ── XHTTP ───────────────────────────────────────────────────────── + case Roles::XhttpModeRole: xhttp.mode = str; + break; + case Roles::XhttpHostRole: xhttp.host = str; + break; + case Roles::XhttpPathRole: xhttp.path = str; + break; + case Roles::XhttpHeadersTemplateRole: xhttp.headersTemplate = str; + break; + case Roles::XhttpUplinkMethodRole: xhttp.uplinkMethod = str; + break; + case Roles::XhttpDisableGrpcRole: xhttp.disableGrpc = value.toBool(); + break; + case Roles::XhttpDisableSseRole: xhttp.disableSse = value.toBool(); + break; + + case Roles::XhttpSessionPlacementRole: xhttp.sessionPlacement = str; + break; + case Roles::XhttpSessionKeyRole: xhttp.sessionKey = str; + break; + case Roles::XhttpSeqPlacementRole: xhttp.seqPlacement = str; + break; + case Roles::XhttpSeqKeyRole: xhttp.seqKey = str; + break; + case Roles::XhttpUplinkDataPlacementRole: xhttp.uplinkDataPlacement = str; + break; + case Roles::XhttpUplinkDataKeyRole: xhttp.uplinkDataKey = str; + break; + + case Roles::XhttpUplinkChunkSizeRole: xhttp.uplinkChunkSize = str; + break; + case Roles::XhttpScMaxBufferedPostsRole: xhttp.scMaxBufferedPosts = str; + break; + case Roles::XhttpScMaxEachPostBytesMinRole: xhttp.scMaxEachPostBytesMin = str; + break; + case Roles::XhttpScMaxEachPostBytesMaxRole: xhttp.scMaxEachPostBytesMax = str; + break; + case Roles::XhttpScMinPostsIntervalMsMinRole: xhttp.scMinPostsIntervalMsMin = str; + break; + case Roles::XhttpScMinPostsIntervalMsMaxRole: xhttp.scMinPostsIntervalMsMax = str; + break; + case Roles::XhttpScStreamUpServerSecsMinRole: xhttp.scStreamUpServerSecsMin = str; + break; + case Roles::XhttpScStreamUpServerSecsMaxRole: xhttp.scStreamUpServerSecsMax = str; + break; + + // ── mKCP ────────────────────────────────────────────────────────── + case Roles::MkcpTtiRole: mkcp.tti = str; + break; + case Roles::MkcpUplinkCapacityRole: mkcp.uplinkCapacity = str; + break; + case Roles::MkcpDownlinkCapacityRole: mkcp.downlinkCapacity = str; + break; + case Roles::MkcpReadBufferSizeRole: mkcp.readBufferSize = str; + break; + case Roles::MkcpWriteBufferSizeRole: mkcp.writeBufferSize = str; + break; + case Roles::MkcpCongestionRole: mkcp.congestion = value.toBool(); + break; + + // ── xPadding ────────────────────────────────────────────────────── + case Roles::XPaddingBytesMinRole: pad.bytesMin = str; + break; + case Roles::XPaddingBytesMaxRole: pad.bytesMax = str; + break; + case Roles::XPaddingObfsModeRole: pad.obfsMode = value.toBool(); + break; + case Roles::XPaddingKeyRole: pad.key = str; + break; + case Roles::XPaddingHeaderRole: pad.header = str; + break; + case Roles::XPaddingPlacementRole: pad.placement = str; + break; + case Roles::XPaddingMethodRole: pad.method = str; + break; + + // ── xmux ────────────────────────────────────────────────────────── + case Roles::XmuxEnabledRole: mux.enabled = value.toBool(); + break; + case Roles::XmuxMaxConcurrencyMinRole: mux.maxConcurrencyMin = str; + break; + case Roles::XmuxMaxConcurrencyMaxRole: mux.maxConcurrencyMax = str; + break; + case Roles::XmuxMaxConnectionsMinRole: mux.maxConnectionsMin = str; + break; + case Roles::XmuxMaxConnectionsMaxRole: mux.maxConnectionsMax = str; + break; + case Roles::XmuxCMaxReuseTimesMinRole: mux.cMaxReuseTimesMin = str; + break; + case Roles::XmuxCMaxReuseTimesMaxRole: mux.cMaxReuseTimesMax = str; + break; + case Roles::XmuxHMaxRequestTimesMinRole: mux.hMaxRequestTimesMin = str; + break; + case Roles::XmuxHMaxRequestTimesMaxRole: mux.hMaxRequestTimesMax = str; + break; + case Roles::XmuxHMaxReusableSecsMinRole: mux.hMaxReusableSecsMin = str; + break; + case Roles::XmuxHMaxReusableSecsMaxRole: mux.hMaxReusableSecsMax = str; + break; + case Roles::XmuxHKeepAlivePeriodRole: mux.hKeepAlivePeriod = str; + break; - switch (role) { - case Roles::SiteRole: m_protocolConfig.serverConfig.site = strValue; break; - case Roles::PortRole: m_protocolConfig.serverConfig.port = strValue; break; default: return false; } - emit dataChanged(index, index, QList { role }); + emit dataChanged(index, index, QList{role}); return true; } -QVariant XrayConfigModel::data(const QModelIndex &index, int role) const +QVariant XrayConfigModel::data(const QModelIndex& index, int role) const { - if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) + { return QVariant(); } - switch (role) { - case Roles::SiteRole: return m_protocolConfig.serverConfig.site; - case Roles::PortRole: return m_protocolConfig.serverConfig.port; + const auto& srv = m_protocolConfig.serverConfig; + const auto& xhttp = srv.xhttp; + const auto& mkcp = srv.mkcp; + const auto& pad = xhttp.xPadding; + const auto& mux = xhttp.xmux; + + switch (role) + { + // ── Main ────────────────────────────────────────────────────────── + case Roles::SiteRole: return srv.site; + case Roles::PortRole: return srv.port; + case Roles::TransportRole: return srv.transport; + case Roles::SecurityRole: return srv.security; + case Roles::FlowRole: return srv.flow; + + // ── Security ────────────────────────────────────────────────────── + case Roles::FingerprintRole: return srv.fingerprint; + case Roles::SniRole: return srv.sni; + case Roles::AlpnRole: return srv.alpn; + + // ── XHTTP ───────────────────────────────────────────────────────── + case Roles::XhttpModeRole: return xhttp.mode; + case Roles::XhttpHostRole: return xhttp.host; + case Roles::XhttpPathRole: return xhttp.path; + case Roles::XhttpHeadersTemplateRole: return xhttp.headersTemplate; + case Roles::XhttpUplinkMethodRole: return xhttp.uplinkMethod; + case Roles::XhttpDisableGrpcRole: return xhttp.disableGrpc; + case Roles::XhttpDisableSseRole: return xhttp.disableSse; + + case Roles::XhttpSessionPlacementRole: return xhttp.sessionPlacement; + case Roles::XhttpSessionKeyRole: return xhttp.sessionKey; + case Roles::XhttpSeqPlacementRole: return xhttp.seqPlacement; + case Roles::XhttpSeqKeyRole: return xhttp.seqKey; + case Roles::XhttpUplinkDataPlacementRole: return xhttp.uplinkDataPlacement; + case Roles::XhttpUplinkDataKeyRole: return xhttp.uplinkDataKey; + + case Roles::XhttpUplinkChunkSizeRole: return xhttp.uplinkChunkSize; + case Roles::XhttpScMaxBufferedPostsRole: return xhttp.scMaxBufferedPosts; + case Roles::XhttpScMaxEachPostBytesMinRole: return xhttp.scMaxEachPostBytesMin; + case Roles::XhttpScMaxEachPostBytesMaxRole: return xhttp.scMaxEachPostBytesMax; + case Roles::XhttpScMinPostsIntervalMsMinRole: return xhttp.scMinPostsIntervalMsMin; + case Roles::XhttpScMinPostsIntervalMsMaxRole: return xhttp.scMinPostsIntervalMsMax; + case Roles::XhttpScStreamUpServerSecsMinRole: return xhttp.scStreamUpServerSecsMin; + case Roles::XhttpScStreamUpServerSecsMaxRole: return xhttp.scStreamUpServerSecsMax; + + // ── mKCP ────────────────────────────────────────────────────────── + case Roles::MkcpTtiRole: return mkcp.tti; + case Roles::MkcpUplinkCapacityRole: return mkcp.uplinkCapacity; + case Roles::MkcpDownlinkCapacityRole: return mkcp.downlinkCapacity; + case Roles::MkcpReadBufferSizeRole: return mkcp.readBufferSize; + case Roles::MkcpWriteBufferSizeRole: return mkcp.writeBufferSize; + case Roles::MkcpCongestionRole: return mkcp.congestion; + + // ── xPadding ────────────────────────────────────────────────────── + case Roles::XPaddingBytesMinRole: return pad.bytesMin; + case Roles::XPaddingBytesMaxRole: return pad.bytesMax; + case Roles::XPaddingObfsModeRole: return pad.obfsMode; + case Roles::XPaddingKeyRole: return pad.key; + case Roles::XPaddingHeaderRole: return pad.header; + case Roles::XPaddingPlacementRole: return pad.placement; + case Roles::XPaddingMethodRole: return pad.method; + + // ── xmux ────────────────────────────────────────────────────────── + case Roles::XmuxEnabledRole: return mux.enabled; + case Roles::XmuxMaxConcurrencyMinRole: return mux.maxConcurrencyMin; + case Roles::XmuxMaxConcurrencyMaxRole: return mux.maxConcurrencyMax; + case Roles::XmuxMaxConnectionsMinRole: return mux.maxConnectionsMin; + case Roles::XmuxMaxConnectionsMaxRole: return mux.maxConnectionsMax; + case Roles::XmuxCMaxReuseTimesMinRole: return mux.cMaxReuseTimesMin; + case Roles::XmuxCMaxReuseTimesMaxRole: return mux.cMaxReuseTimesMax; + case Roles::XmuxHMaxRequestTimesMinRole: return mux.hMaxRequestTimesMin; + case Roles::XmuxHMaxRequestTimesMaxRole: return mux.hMaxRequestTimesMax; + case Roles::XmuxHMaxReusableSecsMinRole: return mux.hMaxReusableSecsMin; + case Roles::XmuxHMaxReusableSecsMaxRole: return mux.hMaxReusableSecsMax; + case Roles::XmuxHKeepAlivePeriodRole: return mux.hKeepAlivePeriod; } return QVariant(); } -void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig) +void XrayConfigModel::updateModel(amnezia::DockerContainer container, + const amnezia::XrayProtocolConfig& protocolConfig) { beginResetModel(); m_container = container; - m_protocolConfig = protocolConfig; - applyDefaultsToServerConfig(m_protocolConfig.serverConfig); - m_originalProtocolConfig = m_protocolConfig; - endResetModel(); } void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config) { - if (config.port.isEmpty()) { + if (config.port.isEmpty()) config.port = protocols::xray::defaultPort; - } - if (config.transportProto.isEmpty()) { - config.transportProto = ProtocolUtils::transportProtoToString( - ProtocolUtils::defaultTransportProto(amnezia::Proto::Xray), amnezia::Proto::Xray); - } - if (config.site.isEmpty()) { + + if (config.site.isEmpty()) config.site = protocols::xray::defaultSite; - } + + if (config.transport.isEmpty()) + config.transport = "raw"; + + if (config.security.isEmpty()) + config.security = "reality"; + + if (config.flow.isEmpty()) + config.flow = "xtls-rprx-vision"; + + if (config.fingerprint.isEmpty()) + config.fingerprint = "Mozilla/5.0"; + + if (config.sni.isEmpty()) + config.sni = "cdn.example.com"; + + if (config.alpn.isEmpty()) + config.alpn = "HTTP/2"; } amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig() { - bool serverSettingsChanged = !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig); - - if (serverSettingsChanged) { + if (!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig)) + { m_protocolConfig.clearClientConfig(); } - return m_protocolConfig; } +bool XrayConfigModel::isServerSettingsEqual() +{ + return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig); +} + QHash XrayConfigModel::roleNames() const { QHash roles; + // Main roles[SiteRole] = "site"; roles[PortRole] = "port"; + roles[TransportRole] = "transport"; + roles[SecurityRole] = "security"; + roles[FlowRole] = "flow"; + + // Security + roles[FingerprintRole] = "fingerprint"; + roles[SniRole] = "sni"; + roles[AlpnRole] = "alpn"; + + // XHTTP + roles[XhttpModeRole] = "xhttpMode"; + roles[XhttpHostRole] = "xhttpHost"; + roles[XhttpPathRole] = "xhttpPath"; + roles[XhttpHeadersTemplateRole] = "xhttpHeadersTemplate"; + roles[XhttpUplinkMethodRole] = "xhttpUplinkMethod"; + roles[XhttpDisableGrpcRole] = "xhttpDisableGrpc"; + roles[XhttpDisableSseRole] = "xhttpDisableSse"; + + roles[XhttpSessionPlacementRole] = "xhttpSessionPlacement"; + roles[XhttpSessionKeyRole] = "xhttpSessionKey"; + roles[XhttpSeqPlacementRole] = "xhttpSeqPlacement"; + roles[XhttpSeqKeyRole] = "xhttpSeqKey"; + roles[XhttpUplinkDataPlacementRole] = "xhttpUplinkDataPlacement"; + roles[XhttpUplinkDataKeyRole] = "xhttpUplinkDataKey"; + + roles[XhttpUplinkChunkSizeRole] = "xhttpUplinkChunkSize"; + roles[XhttpScMaxBufferedPostsRole] = "xhttpScMaxBufferedPosts"; + roles[XhttpScMaxEachPostBytesMinRole] = "xhttpScMaxEachPostBytesMin"; + roles[XhttpScMaxEachPostBytesMaxRole] = "xhttpScMaxEachPostBytesMax"; + roles[XhttpScMinPostsIntervalMsMinRole] = "xhttpScMinPostsIntervalMsMin"; + roles[XhttpScMinPostsIntervalMsMaxRole] = "xhttpScMinPostsIntervalMsMax"; + roles[XhttpScStreamUpServerSecsMinRole] = "xhttpScStreamUpServerSecsMin"; + roles[XhttpScStreamUpServerSecsMaxRole] = "xhttpScStreamUpServerSecsMax"; + + // mKCP + roles[MkcpTtiRole] = "mkcpTti"; + roles[MkcpUplinkCapacityRole] = "mkcpUplinkCapacity"; + roles[MkcpDownlinkCapacityRole] = "mkcpDownlinkCapacity"; + roles[MkcpReadBufferSizeRole] = "mkcpReadBufferSize"; + roles[MkcpWriteBufferSizeRole] = "mkcpWriteBufferSize"; + roles[MkcpCongestionRole] = "mkcpCongestion"; + + // xPadding + roles[XPaddingBytesMinRole] = "xPaddingBytesMin"; + roles[XPaddingBytesMaxRole] = "xPaddingBytesMax"; + roles[XPaddingObfsModeRole] = "xPaddingObfsMode"; + roles[XPaddingKeyRole] = "xPaddingKey"; + roles[XPaddingHeaderRole] = "xPaddingHeader"; + roles[XPaddingPlacementRole] = "xPaddingPlacement"; + roles[XPaddingMethodRole] = "xPaddingMethod"; + + // xmux + roles[XmuxEnabledRole] = "xmuxEnabled"; + roles[XmuxMaxConcurrencyMinRole] = "xmuxMaxConcurrencyMin"; + roles[XmuxMaxConcurrencyMaxRole] = "xmuxMaxConcurrencyMax"; + roles[XmuxMaxConnectionsMinRole] = "xmuxMaxConnectionsMin"; + roles[XmuxMaxConnectionsMaxRole] = "xmuxMaxConnectionsMax"; + roles[XmuxCMaxReuseTimesMinRole] = "xmuxCMaxReuseTimesMin"; + roles[XmuxCMaxReuseTimesMaxRole] = "xmuxCMaxReuseTimesMax"; + roles[XmuxHMaxRequestTimesMinRole] = "xmuxHMaxRequestTimesMin"; + roles[XmuxHMaxRequestTimesMaxRole] = "xmuxHMaxRequestTimesMax"; + roles[XmuxHMaxReusableSecsMinRole] = "xmuxHMaxReusableSecsMin"; + roles[XmuxHMaxReusableSecsMaxRole] = "xmuxHMaxReusableSecsMax"; + roles[XmuxHKeepAlivePeriodRole] = "xmuxHKeepAlivePeriod"; return roles; } diff --git a/client/ui/models/protocols/xrayConfigModel.h b/client/ui/models/protocols/xrayConfigModel.h index 5549cf446..355deb42d 100644 --- a/client/ui/models/protocols/xrayConfigModel.h +++ b/client/ui/models/protocols/xrayConfigModel.h @@ -13,21 +13,90 @@ class XrayConfigModel : public QAbstractListModel Q_OBJECT public: - enum Roles { - SiteRole, - PortRole + enum Roles + { + // ── Main page ───────────────────────────────────────────────── + SiteRole = Qt::UserRole + 1, + PortRole, + TransportRole, // "raw" | "xhttp" | "mkcp" (display in main page row) + SecurityRole, // "none" | "tls" | "reality" (display in main page row) + FlowRole, // "" | "xtls-rprx-vision" | "xtls-rprx-vision-udp443" + + // ── Security ────────────────────────────────────────────────── + FingerprintRole, + SniRole, + AlpnRole, + + // ── Transport — XHTTP ───────────────────────────────────────── + XhttpModeRole, + XhttpHostRole, + XhttpPathRole, + XhttpHeadersTemplateRole, + XhttpUplinkMethodRole, + XhttpDisableGrpcRole, + XhttpDisableSseRole, + + // Session & Sequence + XhttpSessionPlacementRole, + XhttpSessionKeyRole, + XhttpSeqPlacementRole, + XhttpSeqKeyRole, + XhttpUplinkDataPlacementRole, + XhttpUplinkDataKeyRole, + + // Traffic Shaping + XhttpUplinkChunkSizeRole, + XhttpScMaxBufferedPostsRole, + XhttpScMaxEachPostBytesMinRole, + XhttpScMaxEachPostBytesMaxRole, + XhttpScMinPostsIntervalMsMinRole, + XhttpScMinPostsIntervalMsMaxRole, + XhttpScStreamUpServerSecsMinRole, + XhttpScStreamUpServerSecsMaxRole, + + // ── Transport — mKCP ────────────────────────────────────────── + MkcpTtiRole, + MkcpUplinkCapacityRole, + MkcpDownlinkCapacityRole, + MkcpReadBufferSizeRole, + MkcpWriteBufferSizeRole, + MkcpCongestionRole, + + // ── xPadding ────────────────────────────────────────────────── + XPaddingBytesMinRole, + XPaddingBytesMaxRole, + XPaddingObfsModeRole, + XPaddingKeyRole, + XPaddingHeaderRole, + XPaddingPlacementRole, + XPaddingMethodRole, + + // ── xmux ────────────────────────────────────────────────────── + XmuxEnabledRole, + XmuxMaxConcurrencyMinRole, + XmuxMaxConcurrencyMaxRole, + XmuxMaxConnectionsMinRole, + XmuxMaxConnectionsMaxRole, + XmuxCMaxReuseTimesMinRole, + XmuxCMaxReuseTimesMaxRole, + XmuxHMaxRequestTimesMinRole, + XmuxHMaxRequestTimesMaxRole, + XmuxHMaxReusableSecsMinRole, + XmuxHMaxReusableSecsMaxRole, + XmuxHKeepAlivePeriodRole, }; - explicit XrayConfigModel(QObject *parent = nullptr); + explicit XrayConfigModel(QObject* parent = nullptr); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; public slots: - void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig &protocolConfig); + void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig); amnezia::XrayProtocolConfig getProtocolConfig(); + bool isServerSettingsEqual(); protected: QHash roleNames() const override; @@ -36,7 +105,7 @@ private: amnezia::DockerContainer m_container; amnezia::XrayProtocolConfig m_protocolConfig; amnezia::XrayProtocolConfig m_originalProtocolConfig; - + void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config); }; From 57c6f57a129aeb3384bf124a0bc311c4a9c3e76b Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 26 Mar 2026 09:37:16 +0300 Subject: [PATCH 05/18] add new qml padding, update model --- .../core/configurators/xrayConfigurator.cpp | 413 +++++++++++++++--- client/core/configurators/xrayConfigurator.h | 6 + client/core/controllers/coreController.cpp | 3 + client/core/controllers/coreController.h | 2 + .../selfhosted/exportController.cpp | 12 + client/core/installers/xrayInstaller.cpp | 159 ++++++- .../secureAppSettingsRepository.cpp | 8 + .../secureAppSettingsRepository.h | 3 + client/ui/controllers/qml/pageController.h | 1 + .../selfhosted/installUiController.cpp | 3 +- .../ui/models/protocols/xrayConfigModel.cpp | 18 + client/ui/models/protocols/xrayConfigModel.h | 2 + .../ui/models/protocols/xrayConfigsModel.cpp | 195 +++++++++ client/ui/models/protocols/xrayConfigsModel.h | 69 +++ .../PageProtocolXrayConfigsSettings.qml | 148 ++++--- .../Pages2/PageProtocolXrayFlowSettings.qml | 34 +- .../PageProtocolXraySecuritySettings.qml | 135 +++--- .../qml/Pages2/PageProtocolXraySettings.qml | 70 +-- .../PageProtocolXrayTransportSettings.qml | 283 ++++++------ .../PageProtocolXrayXPaddingBytesSettings.qml | 86 ++++ .../PageProtocolXrayXPaddingSettings.qml | 76 ++-- .../Pages2/PageProtocolXrayXmuxSettings.qml | 86 ++-- 22 files changed, 1314 insertions(+), 498 deletions(-) mode change 100755 => 100644 client/ui/controllers/selfhosted/installUiController.cpp create mode 100644 client/ui/models/protocols/xrayConfigsModel.cpp create mode 100644 client/ui/models/protocols/xrayConfigsModel.h create mode 100644 client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml diff --git a/client/core/configurators/xrayConfigurator.cpp b/client/core/configurators/xrayConfigurator.cpp index b525c8991..d16e8ab4b 100644 --- a/client/core/configurators/xrayConfigurator.cpp +++ b/client/core/configurators/xrayConfigurator.cpp @@ -1,23 +1,22 @@ #include "xrayConfigurator.h" +#include "logger.h" #include +#include #include #include -#include #include -#include "logger.h" -#include "core/utils/containerEnum.h" -#include "core/utils/containers/containerUtils.h" -#include "core/utils/protocolEnum.h" -#include "core/utils/selfhosted/sshSession.h" -#include "core/utils/selfhosted/scriptsRegistry.h" -#include "core/utils/protocolEnum.h" +#include "core/models/containerConfig.h" +#include "core/models/protocols/xrayProtocolConfig.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" -#include "core/models/containerConfig.h" -#include "core/models/protocols/xrayProtocolConfig.h" +#include "core/utils/containerEnum.h" +#include "core/utils/containers/containerUtils.h" +#include "core/utils/protocolEnum.h" +#include "core/utils/selfhosted/scriptsRegistry.h" +#include "core/utils/selfhosted/sshSession.h" namespace { Logger logger("XrayConfigurator"); @@ -35,11 +34,19 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia { // Generate new UUID for client QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); - + + // Get flow value from settings (default xtls-rprx-vision) + QString flowValue = "xtls-rprx-vision"; + if (const auto *xrayCfg = containerConfig.protocolConfig.as()) { + if (!xrayCfg->serverConfig.flow.isEmpty()) { + flowValue = xrayCfg->serverConfig.flow; + } + } + // Get current server config QString currentConfig = m_sshSession->getTextFileFromContainer( container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); - + if (errorCode != ErrorCode::NoError) { logger.error() << "Failed to get server config file"; return ""; @@ -54,7 +61,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia } QJsonObject serverConfig = doc.object(); - + // Validate server config structure if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) { logger.error() << "Server config missing 'inbounds' field"; @@ -68,7 +75,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia errorCode = ErrorCode::InternalError; return ""; } - + QJsonObject inbound = inbounds[0].toObject(); if (!inbound.contains(amnezia::protocols::xray::settings)) { logger.error() << "Inbound missing 'settings' field"; @@ -84,26 +91,29 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia } QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray(); - + // Create configuration for new client QJsonObject clientConfig { {amnezia::protocols::xray::id, clientId}, - {amnezia::protocols::xray::flow, "xtls-rprx-vision"} }; - + clientConfig[amnezia::protocols::xray::id] = clientId; + if (!flowValue.isEmpty()) { + clientConfig[amnezia::protocols::xray::flow] = flowValue; + } + clients.append(clientConfig); - + // Update config settings[amnezia::protocols::xray::clients] = clients; inbound[amnezia::protocols::xray::settings] = settings; inbounds[0] = inbound; serverConfig[amnezia::protocols::xray::inbounds] = inbounds; - + // Save updated config to server QString updatedConfig = QJsonDocument(serverConfig).toJson(); errorCode = m_sshSession->uploadTextFileToContainer( - container, - credentials, + container, + credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath, libssh::ScpOverwriteMode::ScpOverwriteExisting @@ -116,7 +126,7 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia // Restart container QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); errorCode = m_sshSession->runScript( - credentials, + credentials, m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns)) ); @@ -128,75 +138,336 @@ QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentia return clientId; } +QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, const QString &clientId) const +{ + QJsonObject streamSettings; + const auto &xhttp = srv.xhttp; + const auto &mkcp = srv.mkcp; + + // network + QString networkValue = "tcp"; + if (srv.transport == "xhttp") + { + networkValue = "xhttp"; + } + else if (srv.transport == "mkcp") + { + networkValue = "kcp"; + } + streamSettings[amnezia::protocols::xray::network] = networkValue; + + // security + streamSettings[amnezia::protocols::xray::security] = srv.security; + + // TLS settings + if (srv.security == "tls") { + QJsonObject tlsSettings; + if (!srv.sni.isEmpty()) { + tlsSettings[amnezia::protocols::xray::serverName] = srv.sni; + } + if (!srv.alpn.isEmpty()) { + QJsonArray alpnArray; + // alpn may be comma-separated: "HTTP/2,HTTP/1.1" + for (const QString &a : srv.alpn.split(",")) { + alpnArray.append(a.trimmed()); + } + tlsSettings["alpn"] = alpnArray; + } + if (!srv.fingerprint.isEmpty()) { + tlsSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint; + } + streamSettings["tlsSettings"] = tlsSettings; + } + + // Reality settings + if (srv.security == "reality") { + QJsonObject realSettings; + if (!srv.fingerprint.isEmpty()) + { + realSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint; + } + if (!srv.sni.isEmpty()) + { + realSettings[amnezia::protocols::xray::serverName] = srv.sni; + } + // publicKey and shortId are filled in createConfig after fetching from server + streamSettings[amnezia::protocols::xray::realitySettings] = realSettings; + } + + // XHTTP transport settings + if (srv.transport == "xhttp") { + QJsonObject xhttpObj; + + if (!xhttp.host.isEmpty()) + { + xhttpObj["host"] = xhttp.host; + } + if (!xhttp.path.isEmpty()) + { + xhttpObj["path"] = xhttp.path; + } + if (!xhttp.mode.isEmpty()) + { + xhttpObj["mode"] = xhttp.mode; + } + + // headers + if (xhttp.headersTemplate == "HTTP") { + QJsonObject headers; + headers["Host"] = xhttp.host; + xhttpObj["headers"] = headers; + } + + if (!xhttp.uplinkMethod.isEmpty()) + { + xhttpObj["method"] = xhttp.uplinkMethod; + } + if (xhttp.disableGrpc) + { + xhttpObj["noGRPCHeader"] = true; + } + if (xhttp.disableSse) + { + xhttpObj["noSSEHeader"] = true; + } + + // Session & Sequence + if (!xhttp.sessionPlacement.isEmpty() && xhttp.sessionPlacement != "None") + { + xhttpObj["scSessionPlacement"] = xhttp.sessionPlacement; + } + if (!xhttp.seqPlacement.isEmpty() && xhttp.seqPlacement != "None") + { + xhttpObj["scSeqPlacement"] = xhttp.seqPlacement; + } + if (!xhttp.uplinkDataPlacement.isEmpty()) + { + xhttpObj["scUplinkDataPlacement"] = xhttp.uplinkDataPlacement; + } + + // Traffic shaping + if (!xhttp.uplinkChunkSize.isEmpty() && xhttp.uplinkChunkSize != "0") + xhttpObj["xhttpUplinkChunkSize"] = xhttp.uplinkChunkSize.toInt(); + if (!xhttp.scMaxBufferedPosts.isEmpty()) + xhttpObj["scMaxBufferedPosts"] = xhttp.scMaxBufferedPosts.toInt(); + + // scMaxEachPostBytes range + if (!xhttp.scMaxEachPostBytesMin.isEmpty() || !xhttp.scMaxEachPostBytesMax.isEmpty()) { + QJsonObject range; + range["from"] = xhttp.scMaxEachPostBytesMin.toInt(); + range["to"] = xhttp.scMaxEachPostBytesMax.toInt(); + xhttpObj["scMaxEachPostBytes"] = range; + } + + // scMinPostsIntervalMs range + if (!xhttp.scMinPostsIntervalMsMin.isEmpty() || !xhttp.scMinPostsIntervalMsMax.isEmpty()) { + QJsonObject range; + range["from"] = xhttp.scMinPostsIntervalMsMin.toInt(); + range["to"] = xhttp.scMinPostsIntervalMsMax.toInt(); + xhttpObj["scMinPostsIntervalMs"] = range; + } + + // scStreamUpServerSecs range + if (!xhttp.scStreamUpServerSecsMin.isEmpty() || !xhttp.scStreamUpServerSecsMax.isEmpty()) { + QJsonObject range; + range["from"] = xhttp.scStreamUpServerSecsMin.toInt(); + range["to"] = xhttp.scStreamUpServerSecsMax.toInt(); + xhttpObj["scStreamUpServerSecs"] = range; + } + + // xPadding + if (xhttp.xPadding.obfsMode) { + QJsonObject paddingObj; + if (!xhttp.xPadding.bytesMin.isEmpty() || !xhttp.xPadding.bytesMax.isEmpty()) { + QJsonObject bytesRange; + bytesRange["from"] = xhttp.xPadding.bytesMin.toInt(); + bytesRange["to"] = xhttp.xPadding.bytesMax.toInt(); + paddingObj["xPaddingBytes"] = bytesRange; + } + if (!xhttp.xPadding.key.isEmpty()) + { + paddingObj["xPaddingKey"] = xhttp.xPadding.key; + } + if (!xhttp.xPadding.header.isEmpty()) + { + paddingObj["xPaddingHeader"] = xhttp.xPadding.header; + } + if (!xhttp.xPadding.placement.isEmpty()) + { + paddingObj["xPaddingPlacement"] = xhttp.xPadding.placement; + } + if (!xhttp.xPadding.method.isEmpty()) + { + paddingObj["xPaddingMethod"] = xhttp.xPadding.method; + } + xhttpObj["xPadding"] = paddingObj; + } + + // xmux + if (xhttp.xmux.enabled) { + QJsonObject muxObj; + muxObj["enabled"] = true; + + auto addRange = [&](const char *key, const QString &minV, const QString &maxV) { + if (!minV.isEmpty() || !maxV.isEmpty()) { + QJsonObject r; + r["from"] = minV.toInt(); + r["to"] = maxV.toInt(); + muxObj[key] = r; + } + }; + + addRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax); + addRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax); + addRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax); + addRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax); + addRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax); + + if (!xhttp.xmux.hKeepAlivePeriod.isEmpty()) + { + muxObj["hKeepAlivePeriod"] = xhttp.xmux.hKeepAlivePeriod.toInt(); + } + + xhttpObj["xmux"] = muxObj; + } + + streamSettings["xhttpSettings"] = xhttpObj; + } + + // mKCP transport settings + if (srv.transport == "mkcp") { + QJsonObject kcpObj; + if (!mkcp.tti.isEmpty()) + { + kcpObj["tti"] = mkcp.tti.toInt(); + } + if (!mkcp.uplinkCapacity.isEmpty()) + { + kcpObj["uplinkCapacity"] = mkcp.uplinkCapacity.toInt(); + } + if (!mkcp.downlinkCapacity.isEmpty()) + { + kcpObj["downlinkCapacity"] = mkcp.downlinkCapacity.toInt(); + } + if (!mkcp.readBufferSize.isEmpty()) + { + kcpObj["readBufferSize"] = mkcp.readBufferSize.toInt(); + } + if (!mkcp.writeBufferSize.isEmpty()) + { + kcpObj["writeBufferSize"] = mkcp.writeBufferSize.toInt(); + } + kcpObj["congestion"] = mkcp.congestion; + streamSettings["kcpSettings"] = kcpObj; + } + + return streamSettings; +} + ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &containerConfig, const DnsSettings &dnsSettings, ErrorCode &errorCode) { - const XrayServerConfig* serverConfig = nullptr; - if (auto* xrayConfig = containerConfig.protocolConfig.as()) { - serverConfig = &xrayConfig->serverConfig; + const XrayServerConfig *serverConfig = nullptr; + if (const auto *xrayCfg = containerConfig.protocolConfig.as()) { + serverConfig = &xrayCfg->serverConfig; } - + + if (!serverConfig) { + logger.error() << "No XrayProtocolConfig found"; + errorCode = ErrorCode::InternalError; + return XrayProtocolConfig {}; + } + + const XrayServerConfig &srv = *serverConfig; + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode); if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { logger.error() << "Failed to prepare server config"; - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; + return XrayProtocolConfig {}; } - amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns); - vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig)); - QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars); - - if (config.isEmpty()) { - logger.error() << "Failed to get config template"; - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; + // Fetch server keys (Reality only) + QString xrayPublicKey; + QString xrayShortId; + + if (srv.security == "reality") { + xrayPublicKey = m_sshSession->getTextFileFromContainer(container, credentials, + amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + return XrayProtocolConfig {}; + } + xrayPublicKey.replace("\n", ""); + + xrayShortId = m_sshSession->getTextFileFromContainer(container, credentials, + amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + return XrayProtocolConfig {}; + } + xrayShortId.replace("\n", ""); } - QString xrayPublicKey = - m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); - if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { - logger.error() << "Failed to get public key"; - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; - } - xrayPublicKey.replace("\n", ""); - - QString xrayShortId = - m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); - if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { - logger.error() << "Failed to get short ID"; - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; - } - xrayShortId.replace("\n", ""); - - if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { - logger.error() << "Config template missing required variables:" - << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") - << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") - << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); - errorCode = ErrorCode::InternalError; - return XrayProtocolConfig{}; + // Build outbound + QJsonObject userObj; + userObj[amnezia::protocols::xray::id] = xrayClientId; + userObj[amnezia::protocols::xray::encryption] = "none"; + if (!srv.flow.isEmpty()) { + userObj[amnezia::protocols::xray::flow] = srv.flow; } - config.replace("$XRAY_CLIENT_ID", xrayClientId); - config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); - config.replace("$XRAY_SHORT_ID", xrayShortId); + QJsonObject vnextEntry; + vnextEntry[amnezia::protocols::xray::address] = credentials.hostName; + vnextEntry[amnezia::protocols::xray::port] = srv.port.toInt(); + vnextEntry[amnezia::protocols::xray::users] = QJsonArray { userObj }; + QJsonObject outboundSettings; + outboundSettings[amnezia::protocols::xray::vnext] = QJsonArray { vnextEntry }; + + QJsonObject outbound; + outbound["protocol"] = "vless"; + outbound[amnezia::protocols::xray::settings] = outboundSettings; + + // Build streamSettings + QJsonObject streamObj = buildStreamSettings(srv, xrayClientId); + + // Inject Reality keys + if (srv.security == "reality") { + QJsonObject rs = streamObj[amnezia::protocols::xray::realitySettings].toObject(); + rs[amnezia::protocols::xray::publicKey] = xrayPublicKey; + rs[amnezia::protocols::xray::shortId] = xrayShortId; + rs[amnezia::protocols::xray::spiderX] = ""; + streamObj[amnezia::protocols::xray::realitySettings] = rs; + } + + outbound[amnezia::protocols::xray::streamSettings] = streamObj; + + // Build full client config + QJsonObject inboundObj; + inboundObj["listen"] = amnezia::protocols::xray::defaultLocalAddr; + inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort; + inboundObj["protocol"] = "socks"; + inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } }; + + QJsonObject clientJson; + clientJson["log"] = QJsonObject { { "loglevel", "error" } }; + clientJson[amnezia::protocols::xray::inbounds] = QJsonArray { inboundObj }; + clientJson[amnezia::protocols::xray::outbounds] = QJsonArray { outbound }; + + QString config = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact)); + + // Return XrayProtocolConfig protocolConfig; - if (serverConfig) { - protocolConfig.serverConfig = *serverConfig; - } - + protocolConfig.serverConfig = srv; + XrayClientConfig clientConfig; clientConfig.nativeConfig = config; - clientConfig.localPort = ""; + clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort); clientConfig.id = xrayClientId; - + protocolConfig.setClientConfig(clientConfig); - + return protocolConfig; } diff --git a/client/core/configurators/xrayConfigurator.h b/client/core/configurators/xrayConfigurator.h index 74a0ea006..fe1c84cbd 100644 --- a/client/core/configurators/xrayConfigurator.h +++ b/client/core/configurators/xrayConfigurator.h @@ -2,11 +2,13 @@ #define XRAY_CONFIGURATOR_H #include +#include #include "configuratorBase.h" #include "core/utils/errorCodes.h" #include "core/utils/routeModes.h" #include "core/utils/commonStructs.h" +#include "core/models/protocols/xrayProtocolConfig.h" class XrayConfigurator : public ConfiguratorBase { @@ -22,6 +24,10 @@ private: QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig, const amnezia::DnsSettings &dnsSettings, amnezia::ErrorCode &errorCode); + + // Builds the native xray "streamSettings" JSON object from XrayServerConfig + QJsonObject buildStreamSettings(const amnezia::XrayServerConfig &srv, + const QString &clientId) const; }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 227850b6d..34d8d7f73 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -87,6 +87,9 @@ void CoreController::initModels() m_xrayConfigModel = new XrayConfigModel(this); setQmlContextProperty("XrayConfigModel", m_xrayConfigModel); + m_xrayConfigsModel = new XrayConfigsModel(m_appSettingsRepository, this); + setQmlContextProperty("XrayConfigsModel", m_xrayConfigsModel); + m_torConfigModel = new TorConfigModel(this); setQmlContextProperty("TorConfigModel", m_torConfigModel); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 0d77c5167..2ec3314df 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -64,6 +64,7 @@ #include "ui/models/protocols/openvpnConfigModel.h" #include "ui/models/protocols/wireguardConfigModel.h" #include "ui/models/protocols/xrayConfigModel.h" +#include "ui/models/protocols/xrayConfigsModel.h" #include "ui/models/protocolsModel.h" #include "ui/models/services/torConfigModel.h" #include "ui/models/serversModel.h" @@ -202,6 +203,7 @@ private: OpenVpnConfigModel* m_openVpnConfigModel; XrayConfigModel* m_xrayConfigModel; + XrayConfigsModel* m_xrayConfigsModel; TorConfigModel* m_torConfigModel; WireGuardConfigModel* m_wireGuardConfigModel; AwgConfigModel* m_awgConfigModel; diff --git a/client/core/controllers/selfhosted/exportController.cpp b/client/core/controllers/selfhosted/exportController.cpp index 917304c49..8b1014806 100644 --- a/client/core/controllers/selfhosted/exportController.cpp +++ b/client/core/controllers/selfhosted/exportController.cpp @@ -295,6 +295,18 @@ ExportController::ExportResult ExportController::generateXrayConfig(int serverIn vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString(); vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome"); vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString(""); + } else if (vlessServer.security == "tls") { + QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject(); + vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString(); + vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString(); + // alpn: serialize array back to comma-separated for VLESS URI + QJsonArray alpnArr = tlsSettings.value("alpn").toArray(); + QStringList alpnList; + for (const QJsonValue &v : alpnArr) { + alpnList << v.toString(); + } + // alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields + // VlessServerObject doesn't have alpn field, so we embed in serverName if needed } result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN"); diff --git a/client/core/installers/xrayInstaller.cpp b/client/core/installers/xrayInstaller.cpp index 12a4b9833..47ddf52a9 100644 --- a/client/core/installers/xrayInstaller.cpp +++ b/client/core/installers/xrayInstaller.cpp @@ -63,18 +63,163 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c } QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject(); - QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject(); - if (!realitySettings.contains(protocols::xray::serverNames)) { - logger.error() << "Settings missing 'serverNames' field"; + auto *xrayConfig = config.getXrayProtocolConfig(); + if (!xrayConfig) { + logger.error() << "No XrayProtocolConfig in ContainerConfig"; return ErrorCode::InternalError; } - QString siteName = realitySettings[protocols::xray::serverNames][0].toString(); + XrayServerConfig &srv = xrayConfig->serverConfig; - if (auto* xrayConfig = config.getXrayProtocolConfig()) { - xrayConfig->serverConfig.site = siteName; + // ── Port ───────────────────────────────────────────────────────── + if (inbound.contains(protocols::xray::port)) { + srv.port = QString::number(inbound[protocols::xray::port].toInt()); } - + + // ── Network (transport) ─────────────────────────────────────────── + QString networkVal = streamSettings.value(protocols::xray::network).toString("tcp"); + if (networkVal == "xhttp") { + srv.transport = "xhttp"; + } else if (networkVal == "kcp") { + srv.transport = "mkcp"; + } else { + srv.transport = "raw"; + } + + // ── Security ────────────────────────────────────────────────────── + srv.security = streamSettings.value(protocols::xray::security).toString("reality"); + + // ── Reality settings ────────────────────────────────────────────── + if (srv.security == "reality") { + QJsonObject rs = streamSettings.value(protocols::xray::realitySettings).toObject(); + + // serverNames array → site + sni + if (rs.contains(protocols::xray::serverNames)) { + QString sniVal = rs[protocols::xray::serverNames].toArray().first().toString(); + srv.sni = sniVal; + srv.site = sniVal; + } else if (rs.contains(protocols::xray::serverName)) { + srv.sni = rs[protocols::xray::serverName].toString(); + srv.site = srv.sni; + } + + srv.fingerprint = rs.value(protocols::xray::fingerprint).toString("Mozilla/5.0"); + } + + // ── TLS settings ────────────────────────────────────────────────── + if (srv.security == "tls") { + QJsonObject tls = streamSettings.value("tlsSettings").toObject(); + srv.sni = tls.value(protocols::xray::serverName).toString(); + srv.fingerprint = tls.value(protocols::xray::fingerprint).toString("Mozilla/5.0"); + + QJsonArray alpnArr = tls.value("alpn").toArray(); + QStringList alpnList; + for (const QJsonValue &v : alpnArr) { + alpnList << v.toString(); + } + srv.alpn = alpnList.join(","); + } + + // ── Flow (from users array) ─────────────────────────────────────── + if (inbound.contains(protocols::xray::settings)) { + QJsonObject s = inbound[protocols::xray::settings].toObject(); + QJsonArray clientsArr = s.value(protocols::xray::clients).toArray(); + if (!clientsArr.isEmpty()) { + srv.flow = clientsArr[0].toObject().value(protocols::xray::flow).toString(); + } + } + + // ── XHTTP settings ──────────────────────────────────────────────── + if (srv.transport == "xhttp") { + QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject(); + srv.xhttp.mode = xhttpObj.value("mode").toString("Auto"); + srv.xhttp.host = xhttpObj.value("host").toString(); + srv.xhttp.path = xhttpObj.value("path").toString(); + srv.xhttp.uplinkMethod = xhttpObj.value("method").toString("POST"); + srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true); + srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true); + + srv.xhttp.sessionPlacement = xhttpObj.value("scSessionPlacement").toString("Path"); + srv.xhttp.seqPlacement = xhttpObj.value("scSeqPlacement").toString("Path"); + srv.xhttp.uplinkDataPlacement = xhttpObj.value("scUplinkDataPlacement").toString("Body"); + + if (xhttpObj.contains("xhttpUplinkChunkSize")) { + srv.xhttp.uplinkChunkSize = QString::number(xhttpObj["xhttpUplinkChunkSize"].toInt()); + } + if (xhttpObj.contains("scMaxBufferedPosts")) { + srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj["scMaxBufferedPosts"].toInt()); + } + + auto readRange = [&](const char *key, QString &minOut, QString &maxOut) { + QJsonObject r = xhttpObj.value(key).toObject(); + if (!r.isEmpty()) { + minOut = QString::number(r.value("from").toInt()); + maxOut = QString::number(r.value("to").toInt()); + } + }; + readRange("scMaxEachPostBytes", srv.xhttp.scMaxEachPostBytesMin, srv.xhttp.scMaxEachPostBytesMax); + readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax); + readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax); + + // xPadding + if (xhttpObj.contains("xPadding")) { + QJsonObject pad = xhttpObj["xPadding"].toObject(); + srv.xhttp.xPadding.obfsMode = true; + srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString(); + srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString(); + srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString("Cookie"); + srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString("Repeat-x"); + QJsonObject bytesRange = pad.value("xPaddingBytes").toObject(); + if (!bytesRange.isEmpty()) { + srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt()); + srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt()); + } + } + + // xmux + if (xhttpObj.contains("xmux")) { + QJsonObject mux = xhttpObj["xmux"].toObject(); + srv.xhttp.xmux.enabled = mux.value("enabled").toBool(true); + + auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) { + QJsonObject r = mux.value(key).toObject(); + if (!r.isEmpty()) { + minOut = QString::number(r.value("from").toInt()); + maxOut = QString::number(r.value("to").toInt()); + } + }; + readMuxRange("maxConcurrency", srv.xhttp.xmux.maxConcurrencyMin, srv.xhttp.xmux.maxConcurrencyMax); + readMuxRange("maxConnections", srv.xhttp.xmux.maxConnectionsMin, srv.xhttp.xmux.maxConnectionsMax); + readMuxRange("cMaxReuseTimes", srv.xhttp.xmux.cMaxReuseTimesMin, srv.xhttp.xmux.cMaxReuseTimesMax); + readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax); + readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax); + + if (mux.contains("hKeepAlivePeriod")) + srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux["hKeepAlivePeriod"].toInt()); + } + } + + // ── mKCP settings ───────────────────────────────────────────────── + if (srv.transport == "mkcp") { + QJsonObject kcp = streamSettings.value("kcpSettings").toObject(); + if (kcp.contains("tti")) { + srv.mkcp.tti = QString::number(kcp["tti"].toInt()); + } + if (kcp.contains("uplinkCapacity")) { + srv.mkcp.uplinkCapacity = QString::number(kcp["uplinkCapacity"].toInt()); + } + if (kcp.contains("downlinkCapacity")) { + srv.mkcp.downlinkCapacity = QString::number(kcp["downlinkCapacity"].toInt()); + } + if (kcp.contains("readBufferSize")) { + srv.mkcp.readBufferSize = QString::number(kcp["readBufferSize"].toInt()); + } + if (kcp.contains("writeBufferSize")) { + srv.mkcp.writeBufferSize = QString::number(kcp["writeBufferSize"].toInt()); + } + srv.mkcp.congestion = kcp.value("congestion").toBool(true); + } + return ErrorCode::NoError; } diff --git a/client/core/repositories/secureAppSettingsRepository.cpp b/client/core/repositories/secureAppSettingsRepository.cpp index 3bc579c74..90d703685 100644 --- a/client/core/repositories/secureAppSettingsRepository.cpp +++ b/client/core/repositories/secureAppSettingsRepository.cpp @@ -450,4 +450,12 @@ void SecureAppSettingsRepository::setInstallationUuid(const QString &uuid) m_settings->setValue("Conf/installationUuid", uuid); } +QByteArray SecureAppSettingsRepository::xraySavedConfigs() const +{ + return value("Xray/savedConfigs").toByteArray(); +} +void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data) +{ + setValue("Xray/savedConfigs", data); +} diff --git a/client/core/repositories/secureAppSettingsRepository.h b/client/core/repositories/secureAppSettingsRepository.h index 54ee0cd07..f95174dd4 100644 --- a/client/core/repositories/secureAppSettingsRepository.h +++ b/client/core/repositories/secureAppSettingsRepository.h @@ -92,6 +92,9 @@ public: QString nextAvailableServerName() const; + QByteArray xraySavedConfigs() const; + void setXraySavedConfigs(const QByteArray &data); + signals: void appLanguageChanged(QLocale locale); void allowedDnsServersChanged(const QStringList &servers); diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 4fc298d81..8eac73c8a 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -88,6 +88,7 @@ namespace PageLoader PageProtocolXrayXPaddingSettings, PageProtocolXrayFlowSettings, PageProtocolXraySecuritySettings, + PageProtocolXrayXPaddingBytesSettings, }; Q_ENUM_NS(PageEnum) diff --git a/client/ui/controllers/selfhosted/installUiController.cpp b/client/ui/controllers/selfhosted/installUiController.cpp old mode 100755 new mode 100644 index b867a0f5d..883085915 --- a/client/ui/controllers/selfhosted/installUiController.cpp +++ b/client/ui/controllers/selfhosted/installUiController.cpp @@ -480,7 +480,8 @@ void InstallUiController::updateProtocolConfigModel(int serverIndex, int contain case Proto::Awg: updateIfPresent(m_awgConfigModel, containerConfig.getAwgProtocolConfig()); break; case Proto::WireGuard: updateIfPresent(m_wireGuardConfigModel, containerConfig.getWireGuardProtocolConfig()); break; case Proto::OpenVpn: updateIfPresent(m_openVpnConfigModel, containerConfig.getOpenVpnProtocolConfig()); break; - case Proto::Xray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break; + case Proto::Xray: + case Proto::SSXray: updateIfPresent(m_xrayConfigModel, containerConfig.getXrayProtocolConfig()); break; case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break; case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break; case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break; diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index 989a691ac..d8a7e8d4f 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -378,3 +378,21 @@ QHash XrayConfigModel::roleNames() const return roles; } + +void XrayConfigModel::resetToDefaults() +{ + beginResetModel(); + m_protocolConfig.serverConfig = amnezia::XrayServerConfig{}; + applyDefaultsToServerConfig(m_protocolConfig.serverConfig); + endResetModel(); +} + +void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig) +{ + beginResetModel(); + m_protocolConfig.serverConfig = serverConfig; + // Clear client config since server settings changed + m_protocolConfig.clearClientConfig(); + m_originalProtocolConfig = m_protocolConfig; + endResetModel(); +} diff --git a/client/ui/models/protocols/xrayConfigModel.h b/client/ui/models/protocols/xrayConfigModel.h index 355deb42d..ac41f228c 100644 --- a/client/ui/models/protocols/xrayConfigModel.h +++ b/client/ui/models/protocols/xrayConfigModel.h @@ -97,6 +97,8 @@ public slots: void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig); amnezia::XrayProtocolConfig getProtocolConfig(); bool isServerSettingsEqual(); + void resetToDefaults(); + void applyServerConfig(const amnezia::XrayServerConfig &serverConfig); protected: QHash roleNames() const override; diff --git a/client/ui/models/protocols/xrayConfigsModel.cpp b/client/ui/models/protocols/xrayConfigsModel.cpp new file mode 100644 index 000000000..4629cb9c4 --- /dev/null +++ b/client/ui/models/protocols/xrayConfigsModel.cpp @@ -0,0 +1,195 @@ +#include "xrayConfigsModel.h" + +#include +#include + +#include "core/repositories/secureAppSettingsRepository.h" +#include "core/utils/constants/configKeys.h" + +QJsonObject XrayConfigSnapshot::toJson() const +{ + QJsonObject obj; + obj["id"] = id; + obj["displayName"] = displayName; + obj["createdAt"] = createdAt.toString(Qt::ISODate); + obj["serverConfig"] = serverConfig.toJson(); + return obj; +} + +XrayConfigSnapshot XrayConfigSnapshot::fromJson(const QJsonObject &json) +{ + XrayConfigSnapshot s; + s.id = json.value("id").toString(); + s.displayName = json.value("displayName").toString(); + s.createdAt = QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODate); + s.serverConfig = amnezia::XrayServerConfig::fromJson(json.value("serverConfig").toObject()); + return s; +} + +XrayConfigsModel::XrayConfigsModel(SecureAppSettingsRepository *appSettings, QObject *parent) + : QAbstractListModel(parent), m_appSettings(appSettings) +{ + loadAll(); +} + +void XrayConfigsModel::loadAll() +{ + m_configs.clear(); + QByteArray raw = m_appSettings->xraySavedConfigs(); + if (raw.isEmpty()) { + return; + } + + QJsonArray arr = QJsonDocument::fromJson(raw).array(); + for (const QJsonValue &v : arr) { + m_configs.append(XrayConfigSnapshot::fromJson(v.toObject())); + } +} + +void XrayConfigsModel::persistAll() +{ + QJsonArray arr; + for (const XrayConfigSnapshot &s : m_configs) { + arr.append(s.toJson()); + } + m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact)); +} + +int XrayConfigsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_configs.size(); +} + +QVariant XrayConfigsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_configs.size()) { + return QVariant(); + } + + const XrayConfigSnapshot &s = m_configs.at(index.row()); + + switch (role) { + case IdRole: { + return s.id; + } + case DisplayNameRole: { + return s.displayName; + } + case CreatedAtRole: { + return s.createdAt.toString("dd.MM.yyyy HH:mm"); + } + } + return QVariant(); +} + +QHash XrayConfigsModel::roleNames() const +{ + QHash roles; + roles[IdRole] = "configId"; + roles[DisplayNameRole] = "configName"; + roles[CreatedAtRole] = "configDate"; + return roles; +} + +void XrayConfigsModel::reload() +{ + beginResetModel(); + loadAll(); + endResetModel(); +} + +void XrayConfigsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig) +{ + XrayConfigSnapshot snapshot; + snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + snapshot.displayName = buildDisplayName(serverConfig); + snapshot.createdAt = QDateTime::currentDateTime(); + snapshot.serverConfig = serverConfig; + + beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size()); + m_configs.append(snapshot); + endInsertRows(); + + persistAll(); +} + +amnezia::XrayServerConfig XrayConfigsModel::applyConfig(int index) const +{ + if (index < 0 || index >= m_configs.size()) { + return amnezia::XrayServerConfig {}; + } + + return m_configs.at(index).serverConfig; +} + +void XrayConfigsModel::removeConfig(int index) +{ + if (index < 0 || index >= m_configs.size()) { + return; + } + + beginRemoveRows(QModelIndex(), index, index); + m_configs.removeAt(index); + endRemoveRows(); + + persistAll(); + emit configRemoved(index); +} + +QString XrayConfigsModel::exportToJson(int index) const +{ + if (index < 0 || index >= m_configs.size()) { + return {}; + } + return QString::fromUtf8(QJsonDocument(m_configs.at(index).toJson()).toJson(QJsonDocument::Indented)); +} + +bool XrayConfigsModel::importFromJson(const QString &jsonString) +{ + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8()); + if (!doc.isObject()) { + emit importFailed(tr("Invalid JSON format")); + return false; + } + + XrayConfigSnapshot snapshot = XrayConfigSnapshot::fromJson(doc.object()); + if (snapshot.id.isEmpty()) { + snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + } + if (snapshot.displayName.isEmpty()) { + snapshot.displayName = buildDisplayName(snapshot.serverConfig); + } + snapshot.createdAt = QDateTime::currentDateTime(); + + beginInsertRows(QModelIndex(), m_configs.size(), m_configs.size()); + m_configs.append(snapshot); + endInsertRows(); + + persistAll(); + return true; +} + +QString XrayConfigsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg) +{ + // Build a human-readable name: "XHTTP TLS Reality", "RAW Reality", etc. + QString transport; + if (cfg.transport == "xhttp") { + transport = "XHTTP"; + } else if (cfg.transport == "mkcp") { + transport = "mKCP"; + } else { + transport = "RAW (TCP)"; + } + + QString security; + if (cfg.security == "tls") { + security = "TLS"; + } else if (cfg.security == "reality") { + security = "Reality"; + } else { + security = "None"; + } + + return QString("%1 %2").arg(transport, security).trimmed(); +} diff --git a/client/ui/models/protocols/xrayConfigsModel.h b/client/ui/models/protocols/xrayConfigsModel.h new file mode 100644 index 000000000..f5eee9c90 --- /dev/null +++ b/client/ui/models/protocols/xrayConfigsModel.h @@ -0,0 +1,69 @@ +#ifndef XRAYCONFIGSMODEL_H +#define XRAYCONFIGSMODEL_H + +#include +#include +#include +#include +#include +#include + +#include "core/models/protocols/xrayProtocolConfig.h" + +class SecureAppSettingsRepository; + +struct XrayConfigSnapshot +{ + QString id; + QString displayName; // auto-generated: "XHTTP TLS Reality", "RAW Reality", etc. + QDateTime createdAt; + amnezia::XrayServerConfig serverConfig; + + QJsonObject toJson() const; + static XrayConfigSnapshot fromJson(const QJsonObject &json); +}; + +class XrayConfigsModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + DisplayNameRole, + CreatedAtRole, // "dd.MM.yyyy HH:mm" + }; + + explicit XrayConfigsModel(SecureAppSettingsRepository *appSettings, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public slots: + void reload(); + + Q_INVOKABLE void createFromCurrent(const amnezia::XrayServerConfig &serverConfig); + Q_INVOKABLE amnezia::XrayServerConfig applyConfig(int index) const; + Q_INVOKABLE void removeConfig(int index); + + Q_INVOKABLE QString exportToJson(int index) const; + Q_INVOKABLE bool importFromJson(const QString &jsonString); + +signals: + void configApplied(int index); + void configRemoved(int index); + void importFailed(const QString &errorMessage); + +protected: + QHash roleNames() const override; + +private: + SecureAppSettingsRepository *m_appSettings; + QVector m_configs; + + void persistAll(); + void loadAll(); + static QString buildDisplayName(const amnezia::XrayServerConfig &cfg); +}; + +#endif // XRAYCONFIGSMODEL_H diff --git a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml index ef3e11c32..ef7e92e39 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml @@ -14,26 +14,11 @@ import "../Components" PageType { id: root - // Temporary model — will be replaced by real XrayConfigsModel - ListModel { - id: configsModel - ListElement { - configName: "XHTTP TLS Reality"; configDate: "24.02.2026 11:12" - } - ListElement { - configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14" - } - ListElement { - configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14" - } - ListElement { - configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:15" - } - } + property string selectedConfigName: "" + property int selectedConfigIndex: -1 - // Currently selected config for the drawer - property string selectedConfigName: "" - property int selectedConfigIndex: -1 + // Reload the list every time we open this page + Component.onCompleted: XrayConfigsModel.reload() BackButtonType { id: backButton @@ -50,11 +35,12 @@ PageType { anchors.left: parent.left anchors.right: parent.right + model: XrayConfigsModel + header: ColumnLayout { width: listView.width spacing: 0 - // ── Header ──────────────────────────────────────────────── Header2TextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -65,32 +51,37 @@ PageType { wrapMode: Text.WordWrap } - // ── Create config from current settings ─────────────────── + // ── Create from current settings ────────────────────────── LabelWithButtonType { Layout.fillWidth: true text: qsTr("Create configuration based on current settings") textMaximumLineCount: 2 rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function () { - // XrayConfigModel.createConfigFromCurrent() + clickedFunction: function() { + XrayConfigsModel.createFromCurrent(XrayConfigModel.getProtocolConfig().serverConfig) } } - DividerType { - } + DividerType {} // ── Export ──────────────────────────────────────────────── LabelWithButtonType { Layout.fillWidth: true text: qsTr("Export settings") rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function () { - // XrayConfigModel.exportSettings() + clickedFunction: function() { + if (root.selectedConfigIndex >= 0) { + var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex) + ExportController.shareText(json, "xray_config.json") + } else if (XrayConfigsModel.rowCount() > 0) { + // Export the first one if none selected + var json = XrayConfigsModel.exportToJson(0) + ExportController.shareText(json, "xray_config.json") + } } } - DividerType { - } + DividerType {} // ── Import ──────────────────────────────────────────────── LabelWithButtonType { @@ -98,15 +89,14 @@ PageType { text: qsTr("Import settings") descriptionText: qsTr("In JSON format") rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function () { - // XrayConfigModel.importSettings() + clickedFunction: function() { + ImportController.importConfig() } } - DividerType { - } + DividerType {} - // ── Configurations section label ────────────────────────── + // ── Section label ───────────────────────────────────────── CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -115,42 +105,64 @@ PageType { Layout.bottomMargin: 8 text: qsTr("Configurations") color: AmneziaStyle.color.mutedGray + visible: XrayConfigsModel.rowCount() > 0 } } - model: configsModel + // ── Empty state ─────────────────────────────────────────────── + footer: ColumnLayout { + width: listView.width + visible: XrayConfigsModel.rowCount() === 0 + spacing: 0 + Item { Layout.preferredHeight: 32 } + + ParagraphTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + text: qsTr("No saved configurations yet.\nCreate one from the current settings.") + color: AmneziaStyle.color.mutedGray + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + } + + // ── Config list items ───────────────────────────────────────── delegate: ColumnLayout { width: listView.width spacing: 0 LabelWithButtonType { Layout.fillWidth: true - text: configName descriptionText: configDate - rightImageSource: "qrc:/images/controls/more-vertical.svg" - - clickedFunction: function () { - root.selectedConfigName = configName + clickedFunction: function() { + root.selectedConfigName = configName root.selectedConfigIndex = index configActionsDrawer.openTriggered() } } - DividerType { - } + DividerType {} + } + } + + // ── Import result handler ───────────────────────────────────────── + Connections { + target: XrayConfigsModel + function onImportFailed(errorMessage) { + PageController.showNotificationMessage(errorMessage) } } // ── Per-config actions drawer ───────────────────────────────────── DrawerType2 { id: configActionsDrawer - parent: root anchors.fill: parent - expandedHeight: root.height * 0.4 + expandedHeight: root.height * 0.35 expandedStateContent: ColumnLayout { id: drawerContent @@ -166,7 +178,7 @@ PageType { BackButtonType { Layout.fillWidth: true Layout.topMargin: 16 - backButtonFunction: function () { + backButtonFunction: function() { configActionsDrawer.closeTriggered() } } @@ -181,37 +193,57 @@ PageType { wrapMode: Text.WordWrap } - // ── Apply config ────────────────────────────────────────── + // Apply LabelWithButtonType { Layout.fillWidth: true text: qsTr("Apply configuration") rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function () { + clickedFunction: function() { configActionsDrawer.closeTriggered() - // XrayConfigModel.applyConfig(root.selectedConfigIndex) + var serverConfig = XrayConfigsModel.applyConfig(root.selectedConfigIndex) + XrayConfigModel.applyServerConfig(serverConfig) + PageController.closePage() } } - DividerType { + DividerType {} + + // Export this config + LabelWithButtonType { + Layout.fillWidth: true + text: qsTr("Export configuration") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + clickedFunction: function() { + configActionsDrawer.closeTriggered() + var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex) + ExportController.shareText(json, "xray_config.json") + } } - // ── Delete config ───────────────────────────────────────── + DividerType {} + + // Delete LabelWithButtonType { Layout.fillWidth: true text: qsTr("Delete configuration") textColor: AmneziaStyle.color.vibrantRed - clickedFunction: function () { + clickedFunction: function() { configActionsDrawer.closeTriggered() - // XrayConfigModel.deleteConfig(root.selectedConfigIndex) + var yesButtonFunction = function() { + XrayConfigsModel.removeConfig(root.selectedConfigIndex) + root.selectedConfigIndex = -1 + root.selectedConfigName = "" + } + showQuestionDrawer( + qsTr("Delete configuration?"), + qsTr("This action cannot be undone."), + qsTr("Delete"), qsTr("Cancel"), + yesButtonFunction, function() {}) } } - DividerType { - } - - Item { - Layout.preferredHeight: 16 - } + DividerType {} + Item { Layout.preferredHeight: 16 } } } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml index 1309b5315..0a52311e2 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml @@ -14,9 +14,6 @@ import "../Components" PageType { id: root - // Temporary local state — will be replaced by model role - property int selectedFlow: 1 // 0=Empty, 1=xtls-rprx-vision, 2=xtls-rprx-vision-udp443 - BackButtonType { id: backButton anchors.top: parent.top @@ -25,17 +22,17 @@ PageType { anchors.topMargin: 20 + PageController.safeAreaTopMargin } - FlickableType { - id: flickable + ListViewType { + id: listView anchors.top: backButton.bottom anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right - contentHeight: mainColumn.implicitHeight - ColumnLayout { - id: mainColumn - width: flickable.width + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width spacing: 0 Header2TextType { @@ -52,8 +49,8 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("Empty") - checked: root.selectedFlow === 0 - onClicked: root.selectedFlow = 0 + checked: flow === "" + onClicked: flow = "" } DividerType { @@ -63,9 +60,9 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("xtls-rprx-vision") - checked: root.selectedFlow === 1 - onClicked: root.selectedFlow = 1 + text: "xtls-rprx-vision" + checked: flow === "xtls-rprx-vision" + onClicked: flow = "xtls-rprx-vision" } DividerType { @@ -75,9 +72,9 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("xtls-rprx-vision-udp443") - checked: root.selectedFlow === 2 - onClicked: root.selectedFlow = 2 + text: "xtls-rprx-vision-udp443" + checked: flow === "xtls-rprx-vision-udp443" + onClicked: flow = "xtls-rprx-vision-udp443" } DividerType { @@ -97,11 +94,10 @@ PageType { anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.leftMargin: 16 anchors.rightMargin: 16 - text: qsTr("Save") onClicked: { forceActiveFocus() - // XrayConfigModel.setFlow(...) + PageController.closePage() } Keys.onEnterPressed: clicked() Keys.onReturnPressed: clicked() diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml index 377e359a8..efbc0aedf 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -14,16 +14,6 @@ import "../Components" PageType { id: root - // Temporary local state — will be replaced by model roles - property int selectedSecurity: 2 // 0=None, 1=TLS, 2=Reality - - // Shared TLS + Reality fields - property string fingerprint: "Mozilla/5.0" - property string serverName: "cdn.example.com" - - // TLS-only fields - property string alpn: "HTTP/2" - BackButtonType { id: backButton anchors.top: parent.top @@ -32,17 +22,17 @@ PageType { anchors.topMargin: 20 + PageController.safeAreaTopMargin } - FlickableType { - id: flickable + ListViewType { + id: listView anchors.top: backButton.bottom anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right - contentHeight: mainColumn.implicitHeight - ColumnLayout { - id: mainColumn - width: flickable.width + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width spacing: 0 Header2TextType { @@ -54,50 +44,45 @@ PageType { text: qsTr("Security") } - // ── Radio: None ─────────────────────────────────────────── VerticalRadioButton { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("None") - checked: root.selectedSecurity === 0 - onClicked: root.selectedSecurity = 0 + checked: security === "none" + onClicked: security = "none" } DividerType { } - // ── Radio: TLS ──────────────────────────────────────────── VerticalRadioButton { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("TLS") - checked: root.selectedSecurity === 1 - onClicked: root.selectedSecurity = 1 + checked: security === "tls" + onClicked: security = "tls" } DividerType { } - // ── Radio: Reality ──────────────────────────────────────── VerticalRadioButton { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("Reality") - checked: root.selectedSecurity === 2 - onClicked: root.selectedSecurity = 2 + checked: security === "reality" + onClicked: security = "reality" } DividerType { } - // ══════════════════════════════════════════════════════════ - // TLS fields (ALPN + Fingerprint + SNI) - // ══════════════════════════════════════════════════════════ + // ── TLS fields ──────────────────────────────────────────── ColumnLayout { - visible: root.selectedSecurity === 1 + visible: security === "tls" Layout.fillWidth: true spacing: 0 @@ -107,7 +92,7 @@ PageType { Layout.topMargin: 16 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.alpn + text: alpn descriptionText: qsTr("ALPN") headerText: qsTr("ALPN") drawerParent: root @@ -125,19 +110,26 @@ PageType { } } clickedFunction: function () { - root.alpn = selectedText + alpn = selectedText tlsAlpnDropDown.text = selectedText tlsAlpnDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.alpn) { + if (model.get(i).name === alpn) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + tlsAlpnDropDown.text = alpn + } + } } DropDownType { @@ -146,7 +138,7 @@ PageType { Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.fingerprint + text: fingerprint descriptionText: qsTr("Fingerprint") headerText: qsTr("Fingerprint") drawerParent: root @@ -157,47 +149,54 @@ PageType { name: "Mozilla/5.0" } ListElement { - name: "Chrome" + name: "chrome" } ListElement { - name: "Firefox" + name: "firefox" } ListElement { - name: "Safari" + name: "safari" } ListElement { - name: "iOS" + name: "ios" } ListElement { - name: "Android" + name: "android" } ListElement { - name: "Edge" + name: "edge" } ListElement { name: "360" } ListElement { - name: "QQ" + name: "qq" } ListElement { - name: "Random" + name: "random" } } clickedFunction: function () { - root.fingerprint = selectedText + fingerprint = selectedText tlsFingerprintDropDown.text = selectedText tlsFingerprintDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.fingerprint) { + if (model.get(i).name === fingerprint) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + tlsFingerprintDropDown.text = fingerprint + } + } } TextFieldWithHeaderType { @@ -206,16 +205,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Server Name (SNI)") - textField.text: root.serverName - textField.onEditingFinished: root.serverName = textField.text + textField.text: sni + textField.onEditingFinished: { + if (textField.text !== sni) sni = textField.text + } } } - // ══════════════════════════════════════════════════════════ - // Reality fields (Fingerprint + SNI) - // ══════════════════════════════════════════════════════════ + // ── Reality fields ──────────────────────────────────────── ColumnLayout { - visible: root.selectedSecurity === 2 + visible: security === "reality" Layout.fillWidth: true spacing: 0 @@ -225,7 +224,7 @@ PageType { Layout.topMargin: 16 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.fingerprint + text: fingerprint descriptionText: qsTr("Fingerprint") headerText: qsTr("Fingerprint") drawerParent: root @@ -236,47 +235,54 @@ PageType { name: "Mozilla/5.0" } ListElement { - name: "Chrome" + name: "chrome" } ListElement { - name: "Firefox" + name: "firefox" } ListElement { - name: "Safari" + name: "safari" } ListElement { - name: "iOS" + name: "ios" } ListElement { - name: "Android" + name: "android" } ListElement { - name: "Edge" + name: "edge" } ListElement { name: "360" } ListElement { - name: "QQ" + name: "qq" } ListElement { - name: "Random" + name: "random" } } clickedFunction: function () { - root.fingerprint = selectedText + fingerprint = selectedText realityFingerprintDropDown.text = selectedText realityFingerprintDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.fingerprint) { + if (model.get(i).name === fingerprint) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + realityFingerprintDropDown.text = fingerprint + } + } } TextFieldWithHeaderType { @@ -285,8 +291,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Server Name (SNI)") - textField.text: root.serverName - textField.onEditingFinished: root.serverName = textField.text + textField.text: sni + textField.onEditingFinished: { + if (textField.text !== sni) sni = textField.text + } } } @@ -304,11 +312,10 @@ PageType { anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.leftMargin: 16 anchors.rightMargin: 16 - text: qsTr("Save") onClicked: { forceActiveFocus() - // XrayConfigModel.setSecurity(...) + PageController.closePage() } Keys.onEnterPressed: clicked() Keys.onReturnPressed: clicked() diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 9739c1317..bc35474c6 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -50,7 +50,6 @@ PageType { spacing: 0 - // ── Header ──────────────────────────────────────────────── RowLayout { Layout.fillWidth: true Layout.leftMargin: 16 @@ -69,22 +68,17 @@ PageType { implicitHeight: 40 image: "qrc:/images/controls/more-vertical.svg" imageColor: AmneziaStyle.color.mutedGray - onClicked: function () { - PageController.goToPage(PageEnum.PageProtocolXrayConfigsSettings) - } + onClicked: PageController.goToPage(PageEnum.PageProtocolXrayConfigsSettings) } } - // ── "More about settings" link ──────────────────────────── LabelTextType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 4 - text: qsTr("More about settings") color: AmneziaStyle.color.burntOrange - MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor @@ -92,43 +86,32 @@ PageType { } } - // ── Port field ──────────────────────────────────────────── TextFieldWithHeaderType { id: portTextField - Layout.fillWidth: true Layout.topMargin: 32 Layout.leftMargin: 16 Layout.rightMargin: 16 - enabled: listView.enabled - headerText: qsTr("Port") textField.text: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } - textField.onEditingFinished: { - if (textField.text !== port) { - port = textField.text - } + if (textField.text !== port) port = textField.text } - checkEmptyText: true } - // ── Transport row ───────────────────────────────────────── LabelWithButtonType { Layout.fillWidth: true Layout.topMargin: 16 - text: qsTr("Transport") - descriptionText: "RAW (TCP)" // TODO: model role + descriptionText: transport rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled - clickedFunction: function () { PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings) } @@ -137,15 +120,12 @@ PageType { DividerType { } - // ── Security row ────────────────────────────────────────── LabelWithButtonType { Layout.fillWidth: true - text: qsTr("Security") - descriptionText: "TLS" // TODO: model role + descriptionText: security rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled - clickedFunction: function () { PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings) } @@ -154,15 +134,12 @@ PageType { DividerType { } - // ── Flow row ────────────────────────────────────────────── LabelWithButtonType { Layout.fillWidth: true - text: qsTr("Flow") - descriptionText: "xtls-rprx-vision" // TODO: model role + descriptionText: flow rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled - clickedFunction: function () { PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings) } @@ -171,83 +148,58 @@ PageType { DividerType { } - // ── Spacer ──────────────────────────────────────────────── Item { - Layout.fillWidth: true - Layout.preferredHeight: 24 + Layout.fillWidth: true; Layout.preferredHeight: 24 } - // ── Save button ─────────────────────────────────────────── BasicButtonType { id: saveButton - Layout.fillWidth: true Layout.bottomMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - enabled: portTextField.errorText === "" - text: qsTr("Save") - onClicked: function () { forceActiveFocus() - var headerText = qsTr("Save settings?") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") - var yesButtonFunction = function () { if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) return } - PageController.goToPage(PageEnum.PageSetupWizardInstalling) InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) } var noButtonFunction = function () { - if (!GC.isMobile()) { - saveButton.forceActiveFocus() - } + if (!GC.isMobile()) saveButton.forceActiveFocus() } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } - Keys.onEnterPressed: saveButton.clicked() Keys.onReturnPressed: saveButton.clicked() } - // ── Reset settings ──────────────────────────────────────── LabelWithButtonType { Layout.fillWidth: true - text: qsTr("Reset settings") textColor: AmneziaStyle.color.vibrantRed visible: listView.enabled - clickedFunction: function () { - var headerText = qsTr("Reset settings?") - var descriptionText = qsTr("All XRay settings will be restored to defaults.") - var yesButtonText = qsTr("Reset") - var noButtonText = qsTr("Cancel") - var yesButtonFunction = function () { XrayConfigModel.resetToDefaults() } - var noButtonFunction = function () { - } - - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, - yesButtonFunction, noButtonFunction) + showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."), + qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function () { + }) } } - // ── Bottom padding ──────────────────────────────────────── Item { - Layout.fillWidth: true - Layout.preferredHeight: 32 + Layout.fillWidth: true; Layout.preferredHeight: 32 } } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml index c29136f4d..8d711c337 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -14,41 +14,6 @@ import "../Components" PageType { id: root - // Temporary local state — will be replaced by model roles - property int selectedTransport: 0 // 0=RAW, 1=XHTTP, 2=mKCP - - // XHTTP fields - property string xhttpMode: "Auto" - property string xhttpHost: "www.googletagmanager.com" - property string xhttpPath: "" - property string xhttpHeadersTemplate: "HTTP" - property string xhttpUplinkMethod: "POST" - property bool xhttpDisableGrpc: true - property bool xhttpDisableSse: true - // Session & Sequence - property string sessionPlacement: "Path" - property string sessionKey: "Path" - property string seqPlacement: "Path" - property string seqKey: "" - property string uplinkDataPlacement: "Body" - property string uplinkDataKey: "" - // Traffic Shaping - property string uplinkChunkSize: "0" - property string scMaxBufferedPosts: "" - property string scMaxEachPostBytesMin: "1" - property string scMaxEachPostBytesMax: "100" - property string scMinPostsIntervalMsMin: "100" - property string scMinPostsIntervalMsMax: "800" - property string scStreamUpServerSecsMin: "1" - property string scStreamUpServerSecsMax: "100" - // mKCP fields - property string mkcpTti: "" - property string mkcpUplinkCapacity: "" - property string mkcpDownlinkCapacity: "" - property string mkcpReadBufferSize: "" - property string mkcpWriteBufferSize: "" - property bool mkcpCongestion: true - BackButtonType { id: backButton anchors.top: parent.top @@ -57,20 +22,19 @@ PageType { anchors.topMargin: 20 + PageController.safeAreaTopMargin } - FlickableType { - id: flickable + ListViewType { + id: listView anchors.top: backButton.bottom anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right - contentHeight: mainColumn.implicitHeight - ColumnLayout { - id: mainColumn - width: flickable.width + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width spacing: 0 - // ── Header ──────────────────────────────────────────────── Header2TextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -80,51 +44,49 @@ PageType { text: qsTr("Transport") } - // ── Radio: RAW (TCP) ────────────────────────────────────── + // ── Radio buttons ───────────────────────────────────────── VerticalRadioButton { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("RAW (TCP)") - checked: root.selectedTransport === 0 - onClicked: root.selectedTransport = 0 + checked: transport === "raw" + onClicked: transport = "raw" } DividerType { } - // ── Radio: XHTTP ────────────────────────────────────────── VerticalRadioButton { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("XHTTP") descriptionText: qsTr("Advanced users") - checked: root.selectedTransport === 1 - onClicked: root.selectedTransport = 1 + checked: transport === "xhttp" + onClicked: transport = "xhttp" } DividerType { } - // ── Radio: mKCP ─────────────────────────────────────────── VerticalRadioButton { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 text: qsTr("mKCP") - checked: root.selectedTransport === 2 - onClicked: root.selectedTransport = 2 + checked: transport === "mkcp" + onClicked: transport = "mkcp" } DividerType { } // ══════════════════════════════════════════════════════════ - // mKCP Settings (visible when mKCP selected) + // mKCP Settings // ══════════════════════════════════════════════════════════ ColumnLayout { - visible: root.selectedTransport === 2 + visible: transport === "mkcp" Layout.fillWidth: true spacing: 0 @@ -144,8 +106,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("TTI") - textField.text: root.mkcpTti - textField.onEditingFinished: root.mkcpTti = textField.text + textField.text: mkcpTti + textField.onEditingFinished: { + if (textField.text !== mkcpTti) mkcpTti = textField.text + } } TextFieldWithHeaderType { @@ -154,8 +118,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("uplinkCapacity") - textField.text: root.mkcpUplinkCapacity - textField.onEditingFinished: root.mkcpUplinkCapacity = textField.text + textField.text: mkcpUplinkCapacity + textField.onEditingFinished: { + if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text + } } TextFieldWithHeaderType { @@ -164,8 +130,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("downlinkCapacity") - textField.text: root.mkcpDownlinkCapacity - textField.onEditingFinished: root.mkcpDownlinkCapacity = textField.text + textField.text: mkcpDownlinkCapacity + textField.onEditingFinished: { + if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text + } } TextFieldWithHeaderType { @@ -174,8 +142,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("readBufferSize") - textField.text: root.mkcpReadBufferSize - textField.onEditingFinished: root.mkcpReadBufferSize = textField.text + textField.text: mkcpReadBufferSize + textField.onEditingFinished: { + if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text + } } TextFieldWithHeaderType { @@ -184,8 +154,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("writeBufferSize") - textField.text: root.mkcpWriteBufferSize - textField.onEditingFinished: root.mkcpWriteBufferSize = textField.text + textField.text: mkcpWriteBufferSize + textField.onEditingFinished: { + if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text + } } SwitcherType { @@ -193,32 +165,30 @@ PageType { Layout.margins: 16 Layout.topMargin: 8 text: qsTr("Congestion") - checked: root.mkcpCongestion - onToggled: root.mkcpCongestion = checked + checked: mkcpCongestion + onToggled: mkcpCongestion = checked } } // ══════════════════════════════════════════════════════════ - // XHTTP Settings (visible when XHTTP selected) + // XHTTP Settings // ══════════════════════════════════════════════════════════ ColumnLayout { - visible: root.selectedTransport === 1 + visible: transport === "xhttp" Layout.fillWidth: true spacing: 0 - // Mode dropdown DropDownType { id: modeDropDown Layout.fillWidth: true Layout.topMargin: 16 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.xhttpMode + text: xhttpMode descriptionText: qsTr("Mode") headerText: qsTr("Mode") drawerParent: root listView: ListViewWithRadioButtonType { - id: modeListView rootWidth: root.width model: ListModel { ListElement { @@ -235,22 +205,28 @@ PageType { } } clickedFunction: function () { - root.xhttpMode = selectedText + xhttpMode = selectedText modeDropDown.text = selectedText modeDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.xhttpMode) { + if (model.get(i).name === xhttpMode) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + modeDropDown.text = xhttpMode + } + } } - // HTTP Profile label CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -267,8 +243,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Host") - textField.text: root.xhttpHost - textField.onEditingFinished: root.xhttpHost = textField.text + textField.text: xhttpHost + textField.onEditingFinished: { + if (textField.text !== xhttpHost) xhttpHost = textField.text + } } TextFieldWithHeaderType { @@ -277,18 +255,19 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Path") - textField.text: root.xhttpPath - textField.onEditingFinished: root.xhttpPath = textField.text + textField.text: xhttpPath + textField.onEditingFinished: { + if (textField.text !== xhttpPath) xhttpPath = textField.text + } } - // Headers template dropdown DropDownType { id: headersDropDown Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.xhttpHeadersTemplate + text: xhttpHeadersTemplate descriptionText: qsTr("Headers template") headerText: qsTr("Headers template") drawerParent: root @@ -303,29 +282,35 @@ PageType { } } clickedFunction: function () { - root.xhttpHeadersTemplate = selectedText + xhttpHeadersTemplate = selectedText headersDropDown.text = selectedText headersDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.xhttpHeadersTemplate) { + if (model.get(i).name === xhttpHeadersTemplate) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + headersDropDown.text = xhttpHeadersTemplate + } + } } - // UplinkHTTPMethod dropdown DropDownType { id: uplinkMethodDropDown Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.xhttpUplinkMethod + text: xhttpUplinkMethod descriptionText: qsTr("UplinkHTTPMethod") headerText: qsTr("UplinkHTTPMethod") drawerParent: root @@ -343,43 +328,48 @@ PageType { } } clickedFunction: function () { - root.xhttpUplinkMethod = selectedText + xhttpUplinkMethod = selectedText uplinkMethodDropDown.text = selectedText uplinkMethodDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.xhttpUplinkMethod) { + if (model.get(i).name === xhttpUplinkMethod) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + uplinkMethodDropDown.text = xhttpUplinkMethod + } + } } - // Disable gRPC Header SwitcherType { Layout.fillWidth: true Layout.margins: 16 Layout.topMargin: 16 text: qsTr("Disable gRPC Header") descriptionText: qsTr("noGRPCHeader") - checked: root.xhttpDisableGrpc - onToggled: root.xhttpDisableGrpc = checked + checked: xhttpDisableGrpc + onToggled: xhttpDisableGrpc = checked } DividerType { } - // Disable SSE Header SwitcherType { Layout.fillWidth: true Layout.margins: 16 text: qsTr("Disable SSE Header") descriptionText: qsTr("noSSEHeader") - checked: root.xhttpDisableSse - onToggled: root.xhttpDisableSse = checked + checked: xhttpDisableSse + onToggled: xhttpDisableSse = checked } DividerType { @@ -402,7 +392,7 @@ PageType { Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.sessionPlacement + text: xhttpSessionPlacement descriptionText: qsTr("SessionPlacement") headerText: qsTr("SessionPlacement") drawerParent: root @@ -423,19 +413,26 @@ PageType { } } clickedFunction: function () { - root.sessionPlacement = selectedText + xhttpSessionPlacement = selectedText sessionPlacementDropDown.text = selectedText sessionPlacementDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.sessionPlacement) { + if (model.get(i).name === xhttpSessionPlacement) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + sessionPlacementDropDown.text = xhttpSessionPlacement + } + } } DropDownType { @@ -444,7 +441,7 @@ PageType { Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.sessionKey + text: xhttpSessionKey descriptionText: qsTr("SessionKey") headerText: qsTr("SessionKey") drawerParent: root @@ -462,19 +459,26 @@ PageType { } } clickedFunction: function () { - root.sessionKey = selectedText + xhttpSessionKey = selectedText sessionKeyDropDown.text = selectedText sessionKeyDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.sessionKey) { + if (model.get(i).name === xhttpSessionKey) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + sessionKeyDropDown.text = xhttpSessionKey + } + } } DropDownType { @@ -483,7 +487,7 @@ PageType { Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.seqPlacement + text: xhttpSeqPlacement descriptionText: qsTr("SeqPlacement") headerText: qsTr("SeqPlacement") drawerParent: root @@ -504,19 +508,26 @@ PageType { } } clickedFunction: function () { - root.seqPlacement = selectedText + xhttpSeqPlacement = selectedText seqPlacementDropDown.text = selectedText seqPlacementDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.seqPlacement) { + if (model.get(i).name === xhttpSeqPlacement) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + seqPlacementDropDown.text = xhttpSeqPlacement + } + } } TextFieldWithHeaderType { @@ -525,8 +536,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("SeqKey") - textField.text: root.seqKey - textField.onEditingFinished: root.seqKey = textField.text + textField.text: xhttpSeqKey + textField.onEditingFinished: { + if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text + } } DropDownType { @@ -535,7 +548,7 @@ PageType { Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.uplinkDataPlacement + text: xhttpUplinkDataPlacement descriptionText: qsTr("UplinkDataPlacement") headerText: qsTr("UplinkDataPlacement") drawerParent: root @@ -550,19 +563,26 @@ PageType { } } clickedFunction: function () { - root.uplinkDataPlacement = selectedText + xhttpUplinkDataPlacement = selectedText uplinkDataPlacementDropDown.text = selectedText uplinkDataPlacementDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.uplinkDataPlacement) { + if (model.get(i).name === xhttpUplinkDataPlacement) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + uplinkDataPlacementDropDown.text = xhttpUplinkDataPlacement + } + } } TextFieldWithHeaderType { @@ -571,8 +591,10 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("UplinkDataKey") - textField.text: root.uplinkDataKey - textField.onEditingFinished: root.uplinkDataKey = textField.text + textField.text: xhttpUplinkDataKey + textField.onEditingFinished: { + if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text + } } // ── Traffic Shaping ─────────────────────────────────── @@ -592,11 +614,13 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("UplinkChunkSize") - textField.text: root.uplinkChunkSize + textField.text: xhttpUplinkChunkSize textField.validator: IntValidator { bottom: 0 } - textField.onEditingFinished: root.uplinkChunkSize = textField.text + textField.onEditingFinished: { + if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text + } } TextFieldWithHeaderType { @@ -605,11 +629,12 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("scMaxBufferedPosts") - textField.text: root.scMaxBufferedPosts - textField.onEditingFinished: root.scMaxBufferedPosts = textField.text + textField.text: xhttpScMaxBufferedPosts + textField.onEditingFinished: { + if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text + } } - // scMaxEachPostBytes — min/max range CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -623,13 +648,12 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.scMaxEachPostBytesMin - maxValue: root.scMaxEachPostBytesMax - onMinChanged: root.scMaxEachPostBytesMin = val - onMaxChanged: root.scMaxEachPostBytesMax = val + minValue: xhttpScMaxEachPostBytesMin + maxValue: xhttpScMaxEachPostBytesMax + onMinChanged: xhttpScMaxEachPostBytesMin = val + onMaxChanged: xhttpScMaxEachPostBytesMax = val } - // scMinPostsIntervalMs — min/max range CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -643,13 +667,12 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.scMinPostsIntervalMsMin - maxValue: root.scMinPostsIntervalMsMax - onMinChanged: root.scMinPostsIntervalMsMin = val - onMaxChanged: root.scMinPostsIntervalMsMax = val + minValue: xhttpScMinPostsIntervalMsMin + maxValue: xhttpScMinPostsIntervalMsMax + onMinChanged: xhttpScMinPostsIntervalMsMin = val + onMaxChanged: xhttpScMinPostsIntervalMsMax = val } - // scStreamUpServerSecs — min/max range CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -663,10 +686,10 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.scStreamUpServerSecsMin - maxValue: root.scStreamUpServerSecsMax - onMinChanged: root.scStreamUpServerSecsMin = val - onMaxChanged: root.scStreamUpServerSecsMax = val + minValue: xhttpScStreamUpServerSecsMin + maxValue: xhttpScStreamUpServerSecsMax + onMinChanged: xhttpScStreamUpServerSecsMin = val + onMaxChanged: xhttpScStreamUpServerSecsMax = val } // ── Padding and multiplexing ────────────────────────── @@ -688,18 +711,20 @@ PageType { PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings) } } + DividerType { } LabelWithButtonType { Layout.fillWidth: true text: qsTr("XMux") - descriptionText: qsTr("On") + descriptionText: xmuxEnabled ? qsTr("On") : qsTr("Off") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function () { PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings) } } + DividerType { } } @@ -710,7 +735,6 @@ PageType { } } - // ── Save button ─────────────────────────────────────────────────── BasicButtonType { id: saveButton anchors.bottom: parent.bottom @@ -719,11 +743,10 @@ PageType { anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.leftMargin: 16 anchors.rightMargin: 16 - text: qsTr("Save") onClicked: { forceActiveFocus() - // XrayConfigModel.setTransport(...) + PageController.closePage() } Keys.onEnterPressed: clicked() Keys.onReturnPressed: clicked() diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml new file mode 100644 index 000000000..303ccebc4 --- /dev/null +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + } + + ListViewType { + id: listView + anchors.top: backButton.bottom + anchors.bottom: saveButton.top + anchors.left: parent.left + anchors.right: parent.right + + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width + spacing: 0 + + Header2TextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 0 + Layout.bottomMargin: 24 + text: qsTr("xPaddingBytes") + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Range") + color: AmneziaStyle.color.mutedGray + } + + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xPaddingBytesMin + maxValue: xPaddingBytesMax + onMinChanged: xPaddingBytesMin = val + onMaxChanged: xPaddingBytesMax = val + } + + Item { Layout.preferredHeight: 16 } + } + } + + BasicButtonType { + id: saveButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + anchors.leftMargin: 16 + anchors.rightMargin: 16 + text: qsTr("Save") + onClicked: { + forceActiveFocus() + PageController.closePage() + } + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml index e99045697..b6eaa3f4a 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -14,14 +14,6 @@ import "../Components" PageType { id: root - // Temporary local state - property string xPaddingBytes: "0—0" - property bool xPaddingObfsMode: true - property string xPaddingKey: "www.googletagmanager.com" - property string xPaddingHeader: "" - property string xPaddingPlacement: "Cookie" - property string xPaddingMethod: "Repeat-x" - BackButtonType { id: backButton anchors.top: parent.top @@ -30,20 +22,19 @@ PageType { anchors.topMargin: 20 + PageController.safeAreaTopMargin } - FlickableType { - id: flickable + ListViewType { + id: listView anchors.top: backButton.bottom anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right - contentHeight: mainColumn.implicitHeight - ColumnLayout { - id: mainColumn - width: flickable.width + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width spacing: 0 - // ── Header ──────────────────────────────────────────────── Header2TextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -53,11 +44,11 @@ PageType { text: qsTr("xPadding") } - // ── xPaddingBytes nav row ───────────────────────────────── + // xPaddingBytes — min/max display row LabelWithButtonType { Layout.fillWidth: true text: qsTr("xPaddingBytes") - descriptionText: root.xPaddingBytes + descriptionText: (xPaddingBytesMin !== "" ? xPaddingBytesMin : "0") + "—" + (xPaddingBytesMax !== "" ? xPaddingBytesMax : "0") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function () { PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings) @@ -67,48 +58,48 @@ PageType { DividerType { } - // ── xPaddingObfsMode switcher ───────────────────────────── SwitcherType { Layout.fillWidth: true Layout.margins: 16 text: qsTr("xPaddingObfsMode") - checked: root.xPaddingObfsMode - onToggled: root.xPaddingObfsMode = checked + checked: xPaddingObfsMode + onToggled: xPaddingObfsMode = checked } DividerType { } - // ── xPaddingKey ─────────────────────────────────────────── TextFieldWithHeaderType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 16 headerText: qsTr("xPaddingKey") - textField.text: root.xPaddingKey - textField.onEditingFinished: root.xPaddingKey = textField.text + textField.text: xPaddingKey + textField.onEditingFinished: { + if (textField.text !== xPaddingKey) xPaddingKey = textField.text + } } - // ── xPaddingHeader ──────────────────────────────────────── TextFieldWithHeaderType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("xPaddingHeader") - textField.text: root.xPaddingHeader - textField.onEditingFinished: root.xPaddingHeader = textField.text + textField.text: xPaddingHeader + textField.onEditingFinished: { + if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text + } } - // ── xPaddingPlacement dropdown ──────────────────────────── DropDownType { id: placementDropDown Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.xPaddingPlacement + text: xPaddingPlacement descriptionText: qsTr("xPaddingPlacement") headerText: qsTr("xPaddingPlacement") drawerParent: root @@ -129,29 +120,35 @@ PageType { } } clickedFunction: function () { - root.xPaddingPlacement = selectedText + xPaddingPlacement = selectedText placementDropDown.text = selectedText placementDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.xPaddingPlacement) { + if (model.get(i).name === xPaddingPlacement) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + placementDropDown.text = xPaddingPlacement + } + } } - // ── xPaddingMethod dropdown ─────────────────────────────── DropDownType { id: methodDropDown Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: root.xPaddingMethod + text: xPaddingMethod descriptionText: qsTr("xPaddingMethod") headerText: qsTr("xPaddingMethod") drawerParent: root @@ -169,19 +166,26 @@ PageType { } } clickedFunction: function () { - root.xPaddingMethod = selectedText + xPaddingMethod = selectedText methodDropDown.text = selectedText methodDropDown.closeTriggered() } Component.onCompleted: { for (var i = 0; i < model.count; i++) { - if (model.get(i).name === root.xPaddingMethod) { + if (model.get(i).name === xPaddingMethod) { selectedIndex = i; break } } } } + Connections { + target: XrayConfigModel + + function onDataChanged() { + methodDropDown.text = xPaddingMethod + } + } } Item { @@ -190,7 +194,6 @@ PageType { } } - // ── Save button ─────────────────────────────────────────────────── BasicButtonType { id: saveButton anchors.bottom: parent.bottom @@ -199,11 +202,10 @@ PageType { anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.leftMargin: 16 anchors.rightMargin: 16 - text: qsTr("Save") onClicked: { forceActiveFocus() - // XrayConfigModel.setXPadding(...) + PageController.closePage() } Keys.onEnterPressed: clicked() Keys.onReturnPressed: clicked() diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml index e998e55f3..58b9572b7 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -14,20 +14,6 @@ import "../Components" PageType { id: root - // Temporary local state - property bool xmuxEnabled: true - property string maxConcurrencyMin: "0" - property string maxConcurrencyMax: "0" - property string maxConnectionsMin: "0" - property string maxConnectionsMax: "0" - property string cMaxReuseTimesMin: "0" - property string cMaxReuseTimesMax: "0" - property string hMaxRequestTimesMin: "0" - property string hMaxRequestTimesMax: "0" - property string hMaxReusableSecsMin: "0" - property string hMaxReusableSecsMax: "0" - property string hKeepAlivePeriod: "" - BackButtonType { id: backButton anchors.top: parent.top @@ -36,20 +22,19 @@ PageType { anchors.topMargin: 20 + PageController.safeAreaTopMargin } - FlickableType { - id: flickable + ListViewType { + id: listView anchors.top: backButton.bottom anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right - contentHeight: mainColumn.implicitHeight - ColumnLayout { - id: mainColumn - width: flickable.width + model: XrayConfigModel + + delegate: ColumnLayout { + width: listView.width spacing: 0 - // ── Header ──────────────────────────────────────────────── Header2TextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -59,23 +44,21 @@ PageType { text: qsTr("xmux") } - // ── xmux master switcher ────────────────────────────────── SwitcherType { Layout.fillWidth: true Layout.margins: 16 text: qsTr("xmux") - checked: root.xmuxEnabled - onToggled: root.xmuxEnabled = checked + checked: xmuxEnabled + onToggled: xmuxEnabled = checked } DividerType { } - // ── Min/Max pairs (only when enabled) ───────────────────── ColumnLayout { Layout.fillWidth: true spacing: 0 - enabled: root.xmuxEnabled + enabled: xmuxEnabled // maxConcurrency CaptionTextType { @@ -91,10 +74,10 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.maxConcurrencyMin - maxValue: root.maxConcurrencyMax - onMinChanged: root.maxConcurrencyMin = val - onMaxChanged: root.maxConcurrencyMax = val + minValue: xmuxMaxConcurrencyMin + maxValue: xmuxMaxConcurrencyMax + onMinChanged: xmuxMaxConcurrencyMin = val + onMaxChanged: xmuxMaxConcurrencyMax = val } // maxConnections @@ -111,10 +94,10 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.maxConnectionsMin - maxValue: root.maxConnectionsMax - onMinChanged: root.maxConnectionsMin = val - onMaxChanged: root.maxConnectionsMax = val + minValue: xmuxMaxConnectionsMin + maxValue: xmuxMaxConnectionsMax + onMinChanged: xmuxMaxConnectionsMin = val + onMaxChanged: xmuxMaxConnectionsMax = val } // cMaxReuseTimes @@ -131,10 +114,10 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.cMaxReuseTimesMin - maxValue: root.cMaxReuseTimesMax - onMinChanged: root.cMaxReuseTimesMin = val - onMaxChanged: root.cMaxReuseTimesMax = val + minValue: xmuxCMaxReuseTimesMin + maxValue: xmuxCMaxReuseTimesMax + onMinChanged: xmuxCMaxReuseTimesMin = val + onMaxChanged: xmuxCMaxReuseTimesMax = val } // hMaxRequestTimes @@ -151,10 +134,10 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.hMaxRequestTimesMin - maxValue: root.hMaxRequestTimesMax - onMinChanged: root.hMaxRequestTimesMin = val - onMaxChanged: root.hMaxRequestTimesMax = val + minValue: xmuxHMaxRequestTimesMin + maxValue: xmuxHMaxRequestTimesMax + onMinChanged: xmuxHMaxRequestTimesMin = val + onMaxChanged: xmuxHMaxRequestTimesMax = val } // hMaxReusableSecs @@ -171,24 +154,25 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - minValue: root.hMaxReusableSecsMin - maxValue: root.hMaxReusableSecsMax - onMinChanged: root.hMaxReusableSecsMin = val - onMaxChanged: root.hMaxReusableSecsMax = val + minValue: xmuxHMaxReusableSecsMin + maxValue: xmuxHMaxReusableSecsMax + onMinChanged: xmuxHMaxReusableSecsMin = val + onMaxChanged: xmuxHMaxReusableSecsMax = val } - // hKeepAlivePeriod — single field TextFieldWithHeaderType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 16 headerText: qsTr("hKeepAlivePeriod") - textField.text: root.hKeepAlivePeriod + textField.text: xmuxHKeepAlivePeriod textField.validator: IntValidator { bottom: 0 } - textField.onEditingFinished: root.hKeepAlivePeriod = textField.text + textField.onEditingFinished: { + if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text + } } } @@ -198,7 +182,6 @@ PageType { } } - // ── Save button ─────────────────────────────────────────────────── BasicButtonType { id: saveButton anchors.bottom: parent.bottom @@ -207,11 +190,10 @@ PageType { anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.leftMargin: 16 anchors.rightMargin: 16 - text: qsTr("Save") onClicked: { forceActiveFocus() - // XrayConfigModel.setXmux(...) + PageController.closePage() } Keys.onEnterPressed: clicked() Keys.onReturnPressed: clicked() From 5734b3a55d3736f4ebe0fd0012e4756318871cf1 Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 26 Mar 2026 11:19:12 +0300 Subject: [PATCH 06/18] update model and export --- .../core/configurators/xrayConfigurator.cpp | 1 + client/ui/controllers/importUiController.cpp | 9 +++++++++ client/ui/controllers/importUiController.h | 1 + .../selfhosted/exportUiController.cpp | 10 ++++++++++ .../selfhosted/exportUiController.h | 1 + .../ui/models/protocols/xrayConfigsModel.cpp | 20 +++++++++++++++++++ client/ui/models/protocols/xrayConfigsModel.h | 5 +++++ .../PageProtocolXrayConfigsSettings.qml | 19 +++++++++++------- client/ui/qml/qml.qrc | 1 + 9 files changed, 60 insertions(+), 7 deletions(-) diff --git a/client/core/configurators/xrayConfigurator.cpp b/client/core/configurators/xrayConfigurator.cpp index d16e8ab4b..bf85d6e37 100644 --- a/client/core/configurators/xrayConfigurator.cpp +++ b/client/core/configurators/xrayConfigurator.cpp @@ -464,6 +464,7 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia XrayClientConfig clientConfig; clientConfig.nativeConfig = config; + qDebug() << "config:" << config; clientConfig.localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort); clientConfig.id = xrayClientId; diff --git a/client/ui/controllers/importUiController.cpp b/client/ui/controllers/importUiController.cpp index ce9b952c1..db81c608c 100644 --- a/client/ui/controllers/importUiController.cpp +++ b/client/ui/controllers/importUiController.cpp @@ -201,3 +201,12 @@ bool ImportUiController::decodeQrCode(const QString &code) return mInstance->parseQrCodeChunk(code); } #endif + +QString ImportUiController::readTextFile(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + return QString::fromUtf8(file.readAll()); +} diff --git a/client/ui/controllers/importUiController.h b/client/ui/controllers/importUiController.h index 853539d05..c527e93c2 100644 --- a/client/ui/controllers/importUiController.h +++ b/client/ui/controllers/importUiController.h @@ -28,6 +28,7 @@ public slots: QString getMaliciousWarningText(); bool isNativeWireGuardConfig(); void processNativeWireGuardConfig(); + QString readTextFile(const QString &fileName); #if defined Q_OS_ANDROID || defined Q_OS_IOS void startDecodingQr(); diff --git a/client/ui/controllers/selfhosted/exportUiController.cpp b/client/ui/controllers/selfhosted/exportUiController.cpp index 4a9f8bfc4..6f49cadc0 100644 --- a/client/ui/controllers/selfhosted/exportUiController.cpp +++ b/client/ui/controllers/selfhosted/exportUiController.cpp @@ -114,3 +114,13 @@ void ExportUiController::applyExportResult(const ExportController::ExportResult emit exportConfigChanged(); } + +void ExportUiController::setConfigFromString(const QString &config, const QString &fileName) +{ + clearPreviousConfig(); + m_config = config; + emit exportConfigChanged(); + if (!fileName.isEmpty()) { + SystemController::saveFile(fileName, m_config); + } +} diff --git a/client/ui/controllers/selfhosted/exportUiController.h b/client/ui/controllers/selfhosted/exportUiController.h index bfc000b0e..8f777da4f 100644 --- a/client/ui/controllers/selfhosted/exportUiController.h +++ b/client/ui/controllers/selfhosted/exportUiController.h @@ -29,6 +29,7 @@ public slots: QList getQrCodes(); void exportConfig(const QString &fileName); + void setConfigFromString(const QString &config, const QString &fileName); void updateClientManagementModel(int serverIndex, int containerIndex); void revokeConfig(int row, int serverIndex, int containerIndex); diff --git a/client/ui/models/protocols/xrayConfigsModel.cpp b/client/ui/models/protocols/xrayConfigsModel.cpp index 4629cb9c4..cd4a95d34 100644 --- a/client/ui/models/protocols/xrayConfigsModel.cpp +++ b/client/ui/models/protocols/xrayConfigsModel.cpp @@ -193,3 +193,23 @@ QString XrayConfigsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg) return QString("%1 %2").arg(transport, security).trimmed(); } + +void XrayConfigsModel::createFromXrayModel(XrayConfigModel *model) +{ + if (!model) { + return; + } + createFromCurrent(model->getProtocolConfig().serverConfig); +} + +void XrayConfigsModel::applyConfigToXrayModel(int index, XrayConfigModel *model) +{ + if (!model) { + return; + } + amnezia::XrayServerConfig cfg = applyConfig(index); + if (cfg.port.isEmpty()) { + return; // guard against invalid index + } + model->applyServerConfig(cfg); +} diff --git a/client/ui/models/protocols/xrayConfigsModel.h b/client/ui/models/protocols/xrayConfigsModel.h index f5eee9c90..a17f416c1 100644 --- a/client/ui/models/protocols/xrayConfigsModel.h +++ b/client/ui/models/protocols/xrayConfigsModel.h @@ -9,6 +9,7 @@ #include #include "core/models/protocols/xrayProtocolConfig.h" +#include "ui/models/protocols/xrayConfigModel.h" class SecureAppSettingsRepository; @@ -49,6 +50,10 @@ public slots: Q_INVOKABLE QString exportToJson(int index) const; Q_INVOKABLE bool importFromJson(const QString &jsonString); + // Convenience: create snapshot from live model, apply snapshot back to model + Q_INVOKABLE void createFromXrayModel(XrayConfigModel *model); + Q_INVOKABLE void applyConfigToXrayModel(int index, XrayConfigModel *model); + signals: void configApplied(int index); void configRemoved(int index); diff --git a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml index ef7e92e39..514d699b9 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml @@ -58,7 +58,7 @@ PageType { textMaximumLineCount: 2 rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - XrayConfigsModel.createFromCurrent(XrayConfigModel.getProtocolConfig().serverConfig) + XrayConfigsModel.createFromXrayModel(XrayConfigModel) } } @@ -72,11 +72,11 @@ PageType { clickedFunction: function() { if (root.selectedConfigIndex >= 0) { var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex) - ExportController.shareText(json, "xray_config.json") + ExportController.setConfigFromString(json, "xray_config.json") } else if (XrayConfigsModel.rowCount() > 0) { // Export the first one if none selected var json = XrayConfigsModel.exportToJson(0) - ExportController.shareText(json, "xray_config.json") + ExportController.setConfigFromString(json, "xray_config.json") } } } @@ -90,7 +90,13 @@ PageType { descriptionText: qsTr("In JSON format") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - ImportController.importConfig() + var filePath = SystemController.getFileName(qsTr("Open XRay config"), qsTr("JSON files (*.json)")) + if (filePath !== "") { + var jsonContent = ImportController.readTextFile(filePath) + if (jsonContent !== "") { + XrayConfigsModel.importFromJson(jsonContent) + } + } } } @@ -200,8 +206,7 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { configActionsDrawer.closeTriggered() - var serverConfig = XrayConfigsModel.applyConfig(root.selectedConfigIndex) - XrayConfigModel.applyServerConfig(serverConfig) + XrayConfigsModel.applyConfigToXrayModel(root.selectedConfigIndex, XrayConfigModel) PageController.closePage() } } @@ -216,7 +221,7 @@ PageType { clickedFunction: function() { configActionsDrawer.closeTriggered() var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex) - ExportController.shareText(json, "xray_config.json") + ExportController.setConfigFromString(json, "xray_config.json") } } diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index d70a531b6..cb84c94ac 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -84,6 +84,7 @@ Pages2/PageProtocolXrayTransportSettings.qml Pages2/PageProtocolXrayXmuxSettings.qml Pages2/PageProtocolXrayXPaddingSettings.qml + Pages2/PageProtocolXrayXPaddingBytesSettings.qml Controls2/MinMaxRowType.qml Pages2/PageServiceDnsSettings.qml From 520b3095d4402047b58197575d133b180e396dd8 Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 26 Mar 2026 13:55:15 +0300 Subject: [PATCH 07/18] rename file & update name class & update list xray --- client/core/controllers/coreController.cpp | 4 +- client/core/controllers/coreController.h | 4 +- client/ui/controllers/qml/pageController.h | 2 +- ...Model.cpp => xrayConfigSnapshotsModel.cpp} | 43 ++++--- ...figsModel.h => xrayConfigSnapshotsModel.h} | 10 +- .../Pages2/PageProtocolXrayFlowSettings.qml | 34 ++--- .../PageProtocolXraySecuritySettings.qml | 35 ++--- .../qml/Pages2/PageProtocolXraySettings.qml | 2 +- ...ings.qml => PageProtocolXraySnapshots.qml} | 121 ++++++++++++------ .../PageProtocolXrayTransportSettings.qml | 35 ++--- .../PageProtocolXrayXPaddingBytesSettings.qml | 38 +++--- .../PageProtocolXrayXPaddingSettings.qml | 34 ++--- .../Pages2/PageProtocolXrayXmuxSettings.qml | 35 ++--- client/ui/qml/qml.qrc | 2 +- 14 files changed, 223 insertions(+), 176 deletions(-) rename client/ui/models/protocols/{xrayConfigsModel.cpp => xrayConfigSnapshotsModel.cpp} (75%) rename client/ui/models/protocols/{xrayConfigsModel.h => xrayConfigSnapshotsModel.h} (82%) rename client/ui/qml/Pages2/{PageProtocolXrayConfigsSettings.qml => PageProtocolXraySnapshots.qml} (68%) diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 34d8d7f73..644ecdd1d 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -87,8 +87,8 @@ void CoreController::initModels() m_xrayConfigModel = new XrayConfigModel(this); setQmlContextProperty("XrayConfigModel", m_xrayConfigModel); - m_xrayConfigsModel = new XrayConfigsModel(m_appSettingsRepository, this); - setQmlContextProperty("XrayConfigsModel", m_xrayConfigsModel); + m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this); + setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel); m_torConfigModel = new TorConfigModel(this); setQmlContextProperty("TorConfigModel", m_torConfigModel); diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 2ec3314df..5f78cb9f0 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -64,7 +64,7 @@ #include "ui/models/protocols/openvpnConfigModel.h" #include "ui/models/protocols/wireguardConfigModel.h" #include "ui/models/protocols/xrayConfigModel.h" -#include "ui/models/protocols/xrayConfigsModel.h" +#include "ui/models/protocols/xrayConfigSnapshotsModel.h" #include "ui/models/protocolsModel.h" #include "ui/models/services/torConfigModel.h" #include "ui/models/serversModel.h" @@ -203,7 +203,7 @@ private: OpenVpnConfigModel* m_openVpnConfigModel; XrayConfigModel* m_xrayConfigModel; - XrayConfigsModel* m_xrayConfigsModel; + XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel; TorConfigModel* m_torConfigModel; WireGuardConfigModel* m_wireGuardConfigModel; AwgConfigModel* m_awgConfigModel; diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 8eac73c8a..045dc9bc3 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -82,7 +82,7 @@ namespace PageLoader PageDevMenu, - PageProtocolXrayConfigsSettings, + PageProtocolXraySnapshots, PageProtocolXrayTransportSettings, PageProtocolXrayXmuxSettings, PageProtocolXrayXPaddingSettings, diff --git a/client/ui/models/protocols/xrayConfigsModel.cpp b/client/ui/models/protocols/xrayConfigSnapshotsModel.cpp similarity index 75% rename from client/ui/models/protocols/xrayConfigsModel.cpp rename to client/ui/models/protocols/xrayConfigSnapshotsModel.cpp index cd4a95d34..8a023212f 100644 --- a/client/ui/models/protocols/xrayConfigsModel.cpp +++ b/client/ui/models/protocols/xrayConfigSnapshotsModel.cpp @@ -1,4 +1,4 @@ -#include "xrayConfigsModel.h" +#include "xrayConfigSnapshotsModel.h" #include #include @@ -26,13 +26,14 @@ XrayConfigSnapshot XrayConfigSnapshot::fromJson(const QJsonObject &json) return s; } -XrayConfigsModel::XrayConfigsModel(SecureAppSettingsRepository *appSettings, QObject *parent) - : QAbstractListModel(parent), m_appSettings(appSettings) +XrayConfigSnapshotsModel::XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, + XrayConfigModel *xrayConfigModel, QObject *parent) + : QAbstractListModel(parent), m_appSettings(appSettings), m_xrayConfigModel(xrayConfigModel) { loadAll(); } -void XrayConfigsModel::loadAll() +void XrayConfigSnapshotsModel::loadAll() { m_configs.clear(); QByteArray raw = m_appSettings->xraySavedConfigs(); @@ -46,7 +47,7 @@ void XrayConfigsModel::loadAll() } } -void XrayConfigsModel::persistAll() +void XrayConfigSnapshotsModel::persistAll() { QJsonArray arr; for (const XrayConfigSnapshot &s : m_configs) { @@ -55,13 +56,13 @@ void XrayConfigsModel::persistAll() m_appSettings->setXraySavedConfigs(QJsonDocument(arr).toJson(QJsonDocument::Compact)); } -int XrayConfigsModel::rowCount(const QModelIndex &parent) const +int XrayConfigSnapshotsModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_configs.size(); } -QVariant XrayConfigsModel::data(const QModelIndex &index, int role) const +QVariant XrayConfigSnapshotsModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_configs.size()) { return QVariant(); @@ -83,7 +84,7 @@ QVariant XrayConfigsModel::data(const QModelIndex &index, int role) const return QVariant(); } -QHash XrayConfigsModel::roleNames() const +QHash XrayConfigSnapshotsModel::roleNames() const { QHash roles; roles[IdRole] = "configId"; @@ -92,14 +93,14 @@ QHash XrayConfigsModel::roleNames() const return roles; } -void XrayConfigsModel::reload() +void XrayConfigSnapshotsModel::reload() { beginResetModel(); loadAll(); endResetModel(); } -void XrayConfigsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig) +void XrayConfigSnapshotsModel::createFromCurrent(const amnezia::XrayServerConfig &serverConfig) { XrayConfigSnapshot snapshot; snapshot.id = QUuid::createUuid().toString(QUuid::WithoutBraces); @@ -114,7 +115,7 @@ void XrayConfigsModel::createFromCurrent(const amnezia::XrayServerConfig &server persistAll(); } -amnezia::XrayServerConfig XrayConfigsModel::applyConfig(int index) const +amnezia::XrayServerConfig XrayConfigSnapshotsModel::applyConfig(int index) const { if (index < 0 || index >= m_configs.size()) { return amnezia::XrayServerConfig {}; @@ -123,7 +124,7 @@ amnezia::XrayServerConfig XrayConfigsModel::applyConfig(int index) const return m_configs.at(index).serverConfig; } -void XrayConfigsModel::removeConfig(int index) +void XrayConfigSnapshotsModel::removeConfig(int index) { if (index < 0 || index >= m_configs.size()) { return; @@ -137,7 +138,7 @@ void XrayConfigsModel::removeConfig(int index) emit configRemoved(index); } -QString XrayConfigsModel::exportToJson(int index) const +QString XrayConfigSnapshotsModel::exportToJson(int index) const { if (index < 0 || index >= m_configs.size()) { return {}; @@ -145,7 +146,7 @@ QString XrayConfigsModel::exportToJson(int index) const return QString::fromUtf8(QJsonDocument(m_configs.at(index).toJson()).toJson(QJsonDocument::Indented)); } -bool XrayConfigsModel::importFromJson(const QString &jsonString) +bool XrayConfigSnapshotsModel::importFromJson(const QString &jsonString) { QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8()); if (!doc.isObject()) { @@ -170,7 +171,7 @@ bool XrayConfigsModel::importFromJson(const QString &jsonString) return true; } -QString XrayConfigsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg) +QString XrayConfigSnapshotsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg) { // Build a human-readable name: "XHTTP TLS Reality", "RAW Reality", etc. QString transport; @@ -194,22 +195,22 @@ QString XrayConfigsModel::buildDisplayName(const amnezia::XrayServerConfig &cfg) return QString("%1 %2").arg(transport, security).trimmed(); } -void XrayConfigsModel::createFromXrayModel(XrayConfigModel *model) +void XrayConfigSnapshotsModel::createFromCurrentModel() { - if (!model) { + if (!m_xrayConfigModel) { return; } - createFromCurrent(model->getProtocolConfig().serverConfig); + createFromCurrent(m_xrayConfigModel->getProtocolConfig().serverConfig); } -void XrayConfigsModel::applyConfigToXrayModel(int index, XrayConfigModel *model) +void XrayConfigSnapshotsModel::applyConfigToCurrentModel(int index) { - if (!model) { + if (!m_xrayConfigModel) { return; } amnezia::XrayServerConfig cfg = applyConfig(index); if (cfg.port.isEmpty()) { return; // guard against invalid index } - model->applyServerConfig(cfg); + m_xrayConfigModel->applyServerConfig(cfg); } diff --git a/client/ui/models/protocols/xrayConfigsModel.h b/client/ui/models/protocols/xrayConfigSnapshotsModel.h similarity index 82% rename from client/ui/models/protocols/xrayConfigsModel.h rename to client/ui/models/protocols/xrayConfigSnapshotsModel.h index a17f416c1..9688cd863 100644 --- a/client/ui/models/protocols/xrayConfigsModel.h +++ b/client/ui/models/protocols/xrayConfigSnapshotsModel.h @@ -24,7 +24,7 @@ struct XrayConfigSnapshot static XrayConfigSnapshot fromJson(const QJsonObject &json); }; -class XrayConfigsModel : public QAbstractListModel +class XrayConfigSnapshotsModel : public QAbstractListModel { Q_OBJECT @@ -35,7 +35,8 @@ public: CreatedAtRole, // "dd.MM.yyyy HH:mm" }; - explicit XrayConfigsModel(SecureAppSettingsRepository *appSettings, QObject *parent = nullptr); + explicit XrayConfigSnapshotsModel(SecureAppSettingsRepository *appSettings, XrayConfigModel *xrayConfigModel, + QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; @@ -51,8 +52,8 @@ public slots: Q_INVOKABLE bool importFromJson(const QString &jsonString); // Convenience: create snapshot from live model, apply snapshot back to model - Q_INVOKABLE void createFromXrayModel(XrayConfigModel *model); - Q_INVOKABLE void applyConfigToXrayModel(int index, XrayConfigModel *model); + Q_INVOKABLE void createFromCurrentModel(); + Q_INVOKABLE void applyConfigToCurrentModel(int index); signals: void configApplied(int index); @@ -64,6 +65,7 @@ protected: private: SecureAppSettingsRepository *m_appSettings; + XrayConfigModel *m_xrayConfigModel; QVector m_configs; void persistAll(); diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml index 0a52311e2..6617b239f 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml @@ -25,7 +25,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -86,20 +86,20 @@ PageType { } } - BasicButtonType { - id: saveButton - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - anchors.leftMargin: 16 - anchors.rightMargin: 16 - text: qsTr("Save") - onClicked: { - forceActiveFocus() - PageController.closePage() - } - Keys.onEnterPressed: clicked() - Keys.onReturnPressed: clicked() - } + // BasicButtonType { + // id: saveButton + // anchors.bottom: parent.bottom + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + // anchors.leftMargin: 16 + // anchors.rightMargin: 16 + // text: qsTr("Save") + // onClicked: { + // forceActiveFocus() + // PageController.closePage() + // } + // Keys.onEnterPressed: clicked() + // Keys.onReturnPressed: clicked() + // } } diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml index efbc0aedf..a1dda29e4 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -25,7 +25,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -304,20 +304,21 @@ PageType { } } - BasicButtonType { - id: saveButton - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - anchors.leftMargin: 16 - anchors.rightMargin: 16 - text: qsTr("Save") - onClicked: { - forceActiveFocus() - PageController.closePage() - } - Keys.onEnterPressed: clicked() - Keys.onReturnPressed: clicked() - } + // BasicButtonType { + // id: saveButton + // anchors.bottom: parent.bottom + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + // anchors.leftMargin: 16 + // anchors.rightMargin: 16 + // text: qsTr("Save") + // onClicked: { + // forceActiveFocus() + // PageController.closePage() + // } + // Keys.onEnterPressed: clicked() + // Keys.onReturnPressed: clicked() + // } } + diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index bc35474c6..59d0a6d9f 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -68,7 +68,7 @@ PageType { implicitHeight: 40 image: "qrc:/images/controls/more-vertical.svg" imageColor: AmneziaStyle.color.mutedGray - onClicked: PageController.goToPage(PageEnum.PageProtocolXrayConfigsSettings) + onClicked: PageController.goToPage(PageEnum.PageProtocolXraySnapshots) } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml b/client/ui/qml/Pages2/PageProtocolXraySnapshots.qml similarity index 68% rename from client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml rename to client/ui/qml/Pages2/PageProtocolXraySnapshots.qml index 514d699b9..525cb2308 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySnapshots.qml @@ -10,15 +10,38 @@ import "../Controls2" import "../Controls2/TextTypes" import "../Config" import "../Components" +import Qt.labs.platform 1.1 PageType { id: root - property string selectedConfigName: "" + property string selectedConfigName: "" property int selectedConfigIndex: -1 // Reload the list every time we open this page - Component.onCompleted: XrayConfigsModel.reload() + Component.onCompleted: XrayConfigSnapshotsModel.reload() + + // ── Save xray config snapshot to file ──────────────────────────── + function saveConfigToFile(json) { + var fileName = "" + if (GC.isMobile()) { + fileName = "amnezia_xray_config.json" + } else { + fileName = SystemController.getFileName( + qsTr("Save XRay configuration"), + qsTr("JSON files (*.json)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/amnezia_xray_config", + true, + ".json") + } + if (fileName !== "") { + PageController.showBusyIndicator(true) + ExportController.setConfigFromString(json, fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Configuration saved")) + } + } + BackButtonType { id: backButton @@ -35,7 +58,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right - model: XrayConfigsModel + model: XrayConfigSnapshotsModel header: ColumnLayout { width: listView.width @@ -57,31 +80,30 @@ PageType { text: qsTr("Create configuration based on current settings") textMaximumLineCount: 2 rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function() { - XrayConfigsModel.createFromXrayModel(XrayConfigModel) + clickedFunction: function () { + XrayConfigSnapshotsModel.createFromCurrentModel() } } - DividerType {} + DividerType { + } // ── Export ──────────────────────────────────────────────── LabelWithButtonType { Layout.fillWidth: true text: qsTr("Export settings") rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function() { - if (root.selectedConfigIndex >= 0) { - var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex) - ExportController.setConfigFromString(json, "xray_config.json") - } else if (XrayConfigsModel.rowCount() > 0) { - // Export the first one if none selected - var json = XrayConfigsModel.exportToJson(0) - ExportController.setConfigFromString(json, "xray_config.json") + clickedFunction: function () { + var idx = root.selectedConfigIndex >= 0 ? root.selectedConfigIndex : 0 + if (listView.count > 0) { + var json = XrayConfigSnapshotsModel.exportToJson(idx) + saveConfigToFile(json) } } } - DividerType {} + DividerType { + } // ── Import ──────────────────────────────────────────────── LabelWithButtonType { @@ -89,18 +111,25 @@ PageType { text: qsTr("Import settings") descriptionText: qsTr("In JSON format") rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function() { - var filePath = SystemController.getFileName(qsTr("Open XRay config"), qsTr("JSON files (*.json)")) + clickedFunction: function () { + var filePath = SystemController.getFileName( + qsTr("Open XRay configuration"), + qsTr("JSON files (*.json)")) if (filePath !== "") { var jsonContent = ImportController.readTextFile(filePath) if (jsonContent !== "") { - XrayConfigsModel.importFromJson(jsonContent) + if (!XrayConfigSnapshotsModel.importFromJson(jsonContent)) { + PageController.showNotificationMessage(qsTr("Failed to import configuration")) + } else { + PageController.showNotificationMessage(qsTr("Configuration imported successfully")) + } } } } } - DividerType {} + DividerType { + } // ── Section label ───────────────────────────────────────── CaptionTextType { @@ -111,17 +140,19 @@ PageType { Layout.bottomMargin: 8 text: qsTr("Configurations") color: AmneziaStyle.color.mutedGray - visible: XrayConfigsModel.rowCount() > 0 + visible: listView.count > 0 } } // ── Empty state ─────────────────────────────────────────────── footer: ColumnLayout { width: listView.width - visible: XrayConfigsModel.rowCount() === 0 + visible: listView.count === 0 spacing: 0 - Item { Layout.preferredHeight: 32 } + Item { + Layout.preferredHeight: 32 + } ParagraphTextType { Layout.fillWidth: true @@ -144,20 +175,22 @@ PageType { text: configName descriptionText: configDate rightImageSource: "qrc:/images/controls/more-vertical.svg" - clickedFunction: function() { - root.selectedConfigName = configName + clickedFunction: function () { + root.selectedConfigName = configName root.selectedConfigIndex = index configActionsDrawer.openTriggered() } } - DividerType {} + DividerType { + } } } // ── Import result handler ───────────────────────────────────────── Connections { - target: XrayConfigsModel + target: XrayConfigSnapshotsModel + function onImportFailed(errorMessage) { PageController.showNotificationMessage(errorMessage) } @@ -184,7 +217,7 @@ PageType { BackButtonType { Layout.fillWidth: true Layout.topMargin: 16 - backButtonFunction: function() { + backButtonFunction: function () { configActionsDrawer.closeTriggered() } } @@ -204,51 +237,57 @@ PageType { Layout.fillWidth: true text: qsTr("Apply configuration") rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function() { + clickedFunction: function () { configActionsDrawer.closeTriggered() - XrayConfigsModel.applyConfigToXrayModel(root.selectedConfigIndex, XrayConfigModel) + XrayConfigSnapshotsModel.applyConfigToCurrentModel(root.selectedConfigIndex) PageController.closePage() } } - DividerType {} + DividerType { + } // Export this config LabelWithButtonType { Layout.fillWidth: true text: qsTr("Export configuration") rightImageSource: "qrc:/images/controls/chevron-right.svg" - clickedFunction: function() { + clickedFunction: function () { configActionsDrawer.closeTriggered() - var json = XrayConfigsModel.exportToJson(root.selectedConfigIndex) - ExportController.setConfigFromString(json, "xray_config.json") + var json = XrayConfigSnapshotsModel.exportToJson(root.selectedConfigIndex) + saveConfigToFile(json) } } - DividerType {} + DividerType { + } // Delete LabelWithButtonType { Layout.fillWidth: true text: qsTr("Delete configuration") textColor: AmneziaStyle.color.vibrantRed - clickedFunction: function() { + clickedFunction: function () { configActionsDrawer.closeTriggered() - var yesButtonFunction = function() { - XrayConfigsModel.removeConfig(root.selectedConfigIndex) + var yesButtonFunction = function () { + XrayConfigSnapshotsModel.removeConfig(root.selectedConfigIndex) root.selectedConfigIndex = -1 - root.selectedConfigName = "" + root.selectedConfigName = "" } showQuestionDrawer( qsTr("Delete configuration?"), qsTr("This action cannot be undone."), qsTr("Delete"), qsTr("Cancel"), - yesButtonFunction, function() {}) + yesButtonFunction, function () { + }) } } - DividerType {} - Item { Layout.preferredHeight: 16 } + DividerType { + } + Item { + Layout.preferredHeight: 16 + } } } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml index 8d711c337..8b78eb14d 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -25,7 +25,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -735,20 +735,21 @@ PageType { } } - BasicButtonType { - id: saveButton - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - anchors.leftMargin: 16 - anchors.rightMargin: 16 - text: qsTr("Save") - onClicked: { - forceActiveFocus() - PageController.closePage() - } - Keys.onEnterPressed: clicked() - Keys.onReturnPressed: clicked() - } + // BasicButtonType { + // id: saveButton + // anchors.bottom: parent.bottom + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + // anchors.leftMargin: 16 + // anchors.rightMargin: 16 + // text: qsTr("Save") + // onClicked: { + // forceActiveFocus() + // PageController.closePage() + // } + // Keys.onEnterPressed: clicked() + // Keys.onReturnPressed: clicked() + // } } + diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml index 303ccebc4..6590ad1ed 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml @@ -25,7 +25,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -63,24 +63,26 @@ PageType { onMaxChanged: xPaddingBytesMax = val } - Item { Layout.preferredHeight: 16 } + Item { + Layout.preferredHeight: 16 + } } } - BasicButtonType { - id: saveButton - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - anchors.leftMargin: 16 - anchors.rightMargin: 16 - text: qsTr("Save") - onClicked: { - forceActiveFocus() - PageController.closePage() - } - Keys.onEnterPressed: clicked() - Keys.onReturnPressed: clicked() - } + // BasicButtonType { + // id: saveButton + // anchors.bottom: parent.bottom + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + // anchors.leftMargin: 16 + // anchors.rightMargin: 16 + // text: qsTr("Save") + // onClicked: { + // forceActiveFocus() + // PageController.closePage() + // } + // Keys.onEnterPressed: clicked() + // Keys.onReturnPressed: clicked() + // } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml index b6eaa3f4a..d2d0ad70d 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -25,7 +25,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -194,20 +194,20 @@ PageType { } } - BasicButtonType { - id: saveButton - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - anchors.leftMargin: 16 - anchors.rightMargin: 16 - text: qsTr("Save") - onClicked: { - forceActiveFocus() - PageController.closePage() - } - Keys.onEnterPressed: clicked() - Keys.onReturnPressed: clicked() - } + // BasicButtonType { + // id: saveButton + // anchors.bottom: parent.bottom + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + // anchors.leftMargin: 16 + // anchors.rightMargin: 16 + // text: qsTr("Save") + // onClicked: { + // forceActiveFocus() + // PageController.closePage() + // } + // Keys.onEnterPressed: clicked() + // Keys.onReturnPressed: clicked() + // } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml index 58b9572b7..8376a5cc2 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -25,7 +25,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -182,20 +182,21 @@ PageType { } } - BasicButtonType { - id: saveButton - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - anchors.leftMargin: 16 - anchors.rightMargin: 16 - text: qsTr("Save") - onClicked: { - forceActiveFocus() - PageController.closePage() - } - Keys.onEnterPressed: clicked() - Keys.onReturnPressed: clicked() - } + // BasicButtonType { + // id: saveButton + // anchors.bottom: parent.bottom + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + // anchors.leftMargin: 16 + // anchors.rightMargin: 16 + // text: qsTr("Save") + // onClicked: { + // forceActiveFocus() + // PageController.closePage() + // } + // Keys.onEnterPressed: clicked() + // Keys.onReturnPressed: clicked() + // } } + diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index cb84c94ac..bbd1d2e32 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -78,7 +78,7 @@ Pages2/PageProtocolWireGuardSettings.qml Pages2/PageProtocolXraySettings.qml - Pages2/PageProtocolXrayConfigsSettings.qml + Pages2/PageProtocolXraySnapshots.qml Pages2/PageProtocolXrayFlowSettings.qml Pages2/PageProtocolXraySecuritySettings.qml Pages2/PageProtocolXrayTransportSettings.qml From 7d8992886a74faf61a3c009e7df98e0970655b61 Mon Sep 17 00:00:00 2001 From: dranik Date: Tue, 31 Mar 2026 14:20:11 +0300 Subject: [PATCH 08/18] fixed ui --- .../core/configurators/xrayConfigurator.cpp | 433 ++++++++++-------- client/core/configurators/xrayConfigurator.h | 3 + client/core/installers/xrayInstaller.cpp | 152 ++++-- .../models/protocols/xrayProtocolConfig.cpp | 66 +-- .../models/protocols/xrayProtocolConfig.h | 47 +- client/core/protocols/xrayProtocol.cpp | 48 +- client/core/protocols/xrayProtocol.h | 3 + .../core/utils/constants/protocolConstants.h | 34 ++ .../ui/models/protocols/xrayConfigModel.cpp | 175 ++++++- client/ui/models/protocols/xrayConfigModel.h | 24 + .../qml/Controls2/TextFieldWithHeaderType.qml | 10 + .../Pages2/PageProtocolXrayFlowSettings.qml | 53 ++- .../PageProtocolXraySecuritySettings.qml | 135 +++--- .../qml/Pages2/PageProtocolXraySettings.qml | 2 +- .../PageProtocolXrayTransportSettings.qml | 203 ++++---- .../PageProtocolXrayXPaddingBytesSettings.qml | 53 ++- .../PageProtocolXrayXPaddingSettings.qml | 82 ++-- .../Pages2/PageProtocolXrayXmuxSettings.qml | 53 ++- 18 files changed, 1004 insertions(+), 572 deletions(-) mode change 100755 => 100644 client/core/protocols/xrayProtocol.cpp diff --git a/client/core/configurators/xrayConfigurator.cpp b/client/core/configurators/xrayConfigurator.cpp index bf85d6e37..e50110067 100644 --- a/client/core/configurators/xrayConfigurator.cpp +++ b/client/core/configurators/xrayConfigurator.cpp @@ -8,6 +8,7 @@ #include #include "core/models/containerConfig.h" +#include "core/models/protocolConfig.h" #include "core/models/protocols/xrayProtocolConfig.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" @@ -18,15 +19,125 @@ #include "core/utils/selfhosted/scriptsRegistry.h" #include "core/utils/selfhosted/sshSession.h" -namespace { -Logger logger("XrayConfigurator"); -} +namespace +{ + Logger logger("XrayConfigurator"); + QString normalizeXhttpMode(const QString &m) + { + const QString t = m.trimmed(); + if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("auto"); + } + if (t.compare(QLatin1String("Packet-up"), Qt::CaseInsensitive) == 0) + return QStringLiteral("packet-up"); + if (t.compare(QLatin1String("Stream-up"), Qt::CaseInsensitive) == 0) + return QStringLiteral("stream-up"); + if (t.compare(QLatin1String("Stream-one"), Qt::CaseInsensitive) == 0) + return QStringLiteral("stream-one"); + return t.toLower(); + } + + // Xray-core: empty → path; "None" in UI → omit (core default path) + QString normalizeSessionSeqPlacement(const QString &p) + { + if (p.isEmpty() || p.compare(QLatin1String("None"), Qt::CaseInsensitive) == 0) + return {}; + return p.toLower(); + } + + QString normalizeUplinkDataPlacement(const QString &p) + { + if (p.isEmpty() || p.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0) + return QStringLiteral("body"); + if (p.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) + return QStringLiteral("auto"); + if (p.compare(QLatin1String("Query"), Qt::CaseInsensitive) == 0) + // "Query" is not valid for uplink payload in splithttp; closest documented mode + return QStringLiteral("header"); + return p.toLower(); + } + + // splithttp: cookie | header | query | queryInHeader (not "body") + QString normalizeXPaddingPlacement(const QString &p) + { + QString t = p.trimmed(); + if (t.isEmpty()) + return QString::fromLatin1(amnezia::protocols::xray::defaultXPaddingPlacement).toLower(); + if (t.compare(QLatin1String("Body"), Qt::CaseInsensitive) == 0) + return QStringLiteral("queryInHeader"); + if (t.contains(QLatin1String("queryInHeader"), Qt::CaseInsensitive) + || t.compare(QLatin1String("Query in header"), Qt::CaseInsensitive) == 0) + return QStringLiteral("queryInHeader"); + return t.toLower(); + } + + // splithttp: repeat-x | tokenish + QString normalizeXPaddingMethod(const QString &m) + { + QString t = m.trimmed(); + if (t.isEmpty() || t.compare(QLatin1String("Repeat-x"), Qt::CaseInsensitive) == 0) + return QStringLiteral("repeat-x"); + if (t.compare(QLatin1String("Tokenish"), Qt::CaseInsensitive) == 0) + return QStringLiteral("tokenish"); + if (t.compare(QLatin1String("Random"), Qt::CaseInsensitive) == 0 + || t.compare(QLatin1String("Zero"), Qt::CaseInsensitive) == 0) + return QStringLiteral("repeat-x"); + return t.toLower(); + } + + void putIntRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin, + const char *fallbackMax) + { + if (minV.isEmpty() && maxV.isEmpty()) + return; + if (minV.isEmpty()) + minV = QString::fromLatin1(fallbackMin); + if (maxV.isEmpty()) + maxV = QString::fromLatin1(fallbackMax); + QJsonObject r; + r[QStringLiteral("from")] = minV.toInt(); + r[QStringLiteral("to")] = maxV.toInt(); + obj[QString::fromUtf8(key)] = r; + } + + // Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here. + void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc) + { + QString c = pc.nativeConfig(); + if (c.isEmpty()) { + return; + } + bool changed = false; + if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint), + Qt::CaseInsensitive); + changed = true; + } + const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr); + const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr); + if (c.contains(legacyListen)) { + c.replace(legacyListen, listenOk); + changed = true; + } + if (changed) { + pc.setNativeConfig(c); + } + } +} // namespace XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent) : ConfiguratorBase(sshSession, parent) { } +amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings, + amnezia::ProtocolConfig protocolConfig) +{ + applyDnsToNativeConfig(settings.dns, protocolConfig); + sanitizeXrayNativeConfig(protocolConfig); + return protocolConfig; +} + QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &containerConfig, const DnsSettings &dnsSettings, @@ -143,222 +254,162 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c QJsonObject streamSettings; const auto &xhttp = srv.xhttp; const auto &mkcp = srv.mkcp; + namespace px = amnezia::protocols::xray; - // network - QString networkValue = "tcp"; - if (srv.transport == "xhttp") - { - networkValue = "xhttp"; - } - else if (srv.transport == "mkcp") - { - networkValue = "kcp"; - } - streamSettings[amnezia::protocols::xray::network] = networkValue; + QString networkValue = QStringLiteral("tcp"); + if (srv.transport == QLatin1String("xhttp")) + networkValue = QStringLiteral("xhttp"); + else if (srv.transport == QLatin1String("mkcp")) + networkValue = QStringLiteral("kcp"); + streamSettings[px::network] = networkValue; - // security - streamSettings[amnezia::protocols::xray::security] = srv.security; + streamSettings[px::security] = srv.security; - // TLS settings - if (srv.security == "tls") { + if (srv.security == QLatin1String("tls")) { QJsonObject tlsSettings; - if (!srv.sni.isEmpty()) { - tlsSettings[amnezia::protocols::xray::serverName] = srv.sni; + const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni; + tlsSettings[px::serverName] = sniEff; + const QString alpnEff = srv.alpn.isEmpty() ? QString::fromLatin1(px::defaultAlpn) : srv.alpn; + QJsonArray alpnArray; + for (const QString &a : alpnEff.split(QLatin1Char(','))) { + const QString t = a.trimmed(); + if (!t.isEmpty()) + alpnArray.append(t); } - if (!srv.alpn.isEmpty()) { - QJsonArray alpnArray; - // alpn may be comma-separated: "HTTP/2,HTTP/1.1" - for (const QString &a : srv.alpn.split(",")) { - alpnArray.append(a.trimmed()); - } - tlsSettings["alpn"] = alpnArray; - } - if (!srv.fingerprint.isEmpty()) { - tlsSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint; - } - streamSettings["tlsSettings"] = tlsSettings; + if (!alpnArray.isEmpty()) + tlsSettings[QStringLiteral("alpn")] = alpnArray; + const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint; + tlsSettings[px::fingerprint] = fpEff; + streamSettings[QStringLiteral("tlsSettings")] = tlsSettings; } - // Reality settings - if (srv.security == "reality") { + if (srv.security == QLatin1String("reality")) { QJsonObject realSettings; - if (!srv.fingerprint.isEmpty()) - { - realSettings[amnezia::protocols::xray::fingerprint] = srv.fingerprint; - } - if (!srv.sni.isEmpty()) - { - realSettings[amnezia::protocols::xray::serverName] = srv.sni; - } - // publicKey and shortId are filled in createConfig after fetching from server - streamSettings[amnezia::protocols::xray::realitySettings] = realSettings; + const QString fpEff = srv.fingerprint.isEmpty() ? QString::fromLatin1(px::defaultFingerprint) : srv.fingerprint; + realSettings[px::fingerprint] = fpEff; + const QString sniEff = srv.sni.isEmpty() ? QString::fromLatin1(px::defaultSni) : srv.sni; + realSettings[px::serverName] = sniEff; + streamSettings[px::realitySettings] = realSettings; } - // XHTTP transport settings - if (srv.transport == "xhttp") { - QJsonObject xhttpObj; - - if (!xhttp.host.isEmpty()) - { - xhttpObj["host"] = xhttp.host; - } + // XHTTP — JSON must match Xray-core SplitHTTPConfig (flat xPadding fields, see transport_internet.go) + if (srv.transport == QLatin1String("xhttp")) { + QJsonObject xo; + const QString hostEff = xhttp.host.isEmpty() ? QString::fromLatin1(px::defaultXhttpHost) : xhttp.host; + xo[QStringLiteral("host")] = hostEff; if (!xhttp.path.isEmpty()) - { - xhttpObj["path"] = xhttp.path; - } - if (!xhttp.mode.isEmpty()) - { - xhttpObj["mode"] = xhttp.mode; - } + xo[QStringLiteral("path")] = xhttp.path; + xo[QStringLiteral("mode")] = normalizeXhttpMode(xhttp.mode); - // headers - if (xhttp.headersTemplate == "HTTP") { + if (xhttp.headersTemplate.compare(QLatin1String("HTTP"), Qt::CaseInsensitive) == 0) { QJsonObject headers; - headers["Host"] = xhttp.host; - xhttpObj["headers"] = headers; + headers[QStringLiteral("Host")] = hostEff; + xo[QStringLiteral("headers")] = headers; } - if (!xhttp.uplinkMethod.isEmpty()) - { - xhttpObj["method"] = xhttp.uplinkMethod; - } - if (xhttp.disableGrpc) - { - xhttpObj["noGRPCHeader"] = true; - } - if (xhttp.disableSse) - { - xhttpObj["noSSEHeader"] = true; + const QString methodEff = + xhttp.uplinkMethod.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkMethod) : xhttp.uplinkMethod; + xo[QStringLiteral("uplinkHTTPMethod")] = methodEff.toUpper(); + + xo[QStringLiteral("noGRPCHeader")] = xhttp.disableGrpc; + xo[QStringLiteral("noSSEHeader")] = xhttp.disableSse; + + const QString sessPl = normalizeSessionSeqPlacement(xhttp.sessionPlacement); + if (!sessPl.isEmpty()) + xo[QStringLiteral("sessionPlacement")] = sessPl; + const QString seqPl = normalizeSessionSeqPlacement(xhttp.seqPlacement); + if (!seqPl.isEmpty()) + xo[QStringLiteral("seqPlacement")] = seqPl; + if (!xhttp.sessionKey.isEmpty()) + xo[QStringLiteral("sessionKey")] = xhttp.sessionKey; + if (!xhttp.seqKey.isEmpty()) + xo[QStringLiteral("seqKey")] = xhttp.seqKey; + + xo[QStringLiteral("uplinkDataPlacement")] = normalizeUplinkDataPlacement(xhttp.uplinkDataPlacement); + if (!xhttp.uplinkDataKey.isEmpty()) + xo[QStringLiteral("uplinkDataKey")] = xhttp.uplinkDataKey; + + const QString ucs = xhttp.uplinkChunkSize.isEmpty() ? QString::fromLatin1(px::defaultXhttpUplinkChunkSize) + : xhttp.uplinkChunkSize; + if (!ucs.isEmpty() && ucs != QLatin1String("0")) { + const int v = ucs.toInt(); + QJsonObject chunkR; + chunkR[QStringLiteral("from")] = v; + chunkR[QStringLiteral("to")] = v; + xo[QStringLiteral("uplinkChunkSize")] = chunkR; } - // Session & Sequence - if (!xhttp.sessionPlacement.isEmpty() && xhttp.sessionPlacement != "None") - { - xhttpObj["scSessionPlacement"] = xhttp.sessionPlacement; - } - if (!xhttp.seqPlacement.isEmpty() && xhttp.seqPlacement != "None") - { - xhttpObj["scSeqPlacement"] = xhttp.seqPlacement; - } - if (!xhttp.uplinkDataPlacement.isEmpty()) - { - xhttpObj["scUplinkDataPlacement"] = xhttp.uplinkDataPlacement; - } - - // Traffic shaping - if (!xhttp.uplinkChunkSize.isEmpty() && xhttp.uplinkChunkSize != "0") - xhttpObj["xhttpUplinkChunkSize"] = xhttp.uplinkChunkSize.toInt(); if (!xhttp.scMaxBufferedPosts.isEmpty()) - xhttpObj["scMaxBufferedPosts"] = xhttp.scMaxBufferedPosts.toInt(); + xo[QStringLiteral("scMaxBufferedPosts")] = xhttp.scMaxBufferedPosts.toLongLong(); - // scMaxEachPostBytes range - if (!xhttp.scMaxEachPostBytesMin.isEmpty() || !xhttp.scMaxEachPostBytesMax.isEmpty()) { - QJsonObject range; - range["from"] = xhttp.scMaxEachPostBytesMin.toInt(); - range["to"] = xhttp.scMaxEachPostBytesMax.toInt(); - xhttpObj["scMaxEachPostBytes"] = range; + putIntRangeIfAny(xo, "scMaxEachPostBytes", xhttp.scMaxEachPostBytesMin, xhttp.scMaxEachPostBytesMax, + px::defaultXhttpScMaxEachPostBytesMin, px::defaultXhttpScMaxEachPostBytesMax); + putIntRangeIfAny(xo, "scMinPostsIntervalMs", xhttp.scMinPostsIntervalMsMin, xhttp.scMinPostsIntervalMsMax, + px::defaultXhttpScMinPostsIntervalMsMin, px::defaultXhttpScMinPostsIntervalMsMax); + putIntRangeIfAny(xo, "scStreamUpServerSecs", xhttp.scStreamUpServerSecsMin, xhttp.scStreamUpServerSecsMax, + px::defaultXhttpScStreamUpServerSecsMin, px::defaultXhttpScStreamUpServerSecsMax); + + const auto &pad = xhttp.xPadding; + xo[QStringLiteral("xPaddingObfsMode")] = pad.obfsMode; + if (pad.obfsMode) { + if (!pad.bytesMin.isEmpty() || !pad.bytesMax.isEmpty()) { + QJsonObject br; + br[QStringLiteral("from")] = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt(); + br[QStringLiteral("to")] = pad.bytesMax.isEmpty() ? (pad.bytesMin.isEmpty() ? 256 : pad.bytesMin.toInt()) + : pad.bytesMax.toInt(); + xo[QStringLiteral("xPaddingBytes")] = br; + } + xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key; + xo[QStringLiteral("xPaddingHeader")] = pad.header.isEmpty() ? QStringLiteral("X-Padding") : pad.header; + xo[QStringLiteral("xPaddingPlacement")] = normalizeXPaddingPlacement( + pad.placement.isEmpty() ? QString::fromLatin1(px::defaultXPaddingPlacement) : pad.placement); + xo[QStringLiteral("xPaddingMethod")] = normalizeXPaddingMethod( + pad.method.isEmpty() ? QString::fromLatin1(px::defaultXPaddingMethod) : pad.method); } - // scMinPostsIntervalMs range - if (!xhttp.scMinPostsIntervalMsMin.isEmpty() || !xhttp.scMinPostsIntervalMsMax.isEmpty()) { - QJsonObject range; - range["from"] = xhttp.scMinPostsIntervalMsMin.toInt(); - range["to"] = xhttp.scMinPostsIntervalMsMax.toInt(); - xhttpObj["scMinPostsIntervalMs"] = range; - } - - // scStreamUpServerSecs range - if (!xhttp.scStreamUpServerSecsMin.isEmpty() || !xhttp.scStreamUpServerSecsMax.isEmpty()) { - QJsonObject range; - range["from"] = xhttp.scStreamUpServerSecsMin.toInt(); - range["to"] = xhttp.scStreamUpServerSecsMax.toInt(); - xhttpObj["scStreamUpServerSecs"] = range; - } - - // xPadding - if (xhttp.xPadding.obfsMode) { - QJsonObject paddingObj; - if (!xhttp.xPadding.bytesMin.isEmpty() || !xhttp.xPadding.bytesMax.isEmpty()) { - QJsonObject bytesRange; - bytesRange["from"] = xhttp.xPadding.bytesMin.toInt(); - bytesRange["to"] = xhttp.xPadding.bytesMax.toInt(); - paddingObj["xPaddingBytes"] = bytesRange; - } - if (!xhttp.xPadding.key.isEmpty()) - { - paddingObj["xPaddingKey"] = xhttp.xPadding.key; - } - if (!xhttp.xPadding.header.isEmpty()) - { - paddingObj["xPaddingHeader"] = xhttp.xPadding.header; - } - if (!xhttp.xPadding.placement.isEmpty()) - { - paddingObj["xPaddingPlacement"] = xhttp.xPadding.placement; - } - if (!xhttp.xPadding.method.isEmpty()) - { - paddingObj["xPaddingMethod"] = xhttp.xPadding.method; - } - xhttpObj["xPadding"] = paddingObj; - } - - // xmux + // xmux: Xray has no "enabled" flag; omit object when UI disables multiplex tuning. if (xhttp.xmux.enabled) { - QJsonObject muxObj; - muxObj["enabled"] = true; - - auto addRange = [&](const char *key, const QString &minV, const QString &maxV) { - if (!minV.isEmpty() || !maxV.isEmpty()) { - QJsonObject r; - r["from"] = minV.toInt(); - r["to"] = maxV.toInt(); - muxObj[key] = r; - } + QJsonObject mux; + auto addMuxRange = [&](const char *key, const QString &a, const QString &b) { + if (a.isEmpty() && b.isEmpty()) + return; + QJsonObject r; + r[QStringLiteral("from")] = a.isEmpty() ? 0 : a.toInt(); + r[QStringLiteral("to")] = b.isEmpty() ? 0 : b.toInt(); + mux[QString::fromUtf8(key)] = r; }; - - addRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax); - addRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax); - addRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax); - addRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax); - addRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax); - + addMuxRange("maxConcurrency", xhttp.xmux.maxConcurrencyMin, xhttp.xmux.maxConcurrencyMax); + addMuxRange("maxConnections", xhttp.xmux.maxConnectionsMin, xhttp.xmux.maxConnectionsMax); + addMuxRange("cMaxReuseTimes", xhttp.xmux.cMaxReuseTimesMin, xhttp.xmux.cMaxReuseTimesMax); + addMuxRange("hMaxRequestTimes", xhttp.xmux.hMaxRequestTimesMin, xhttp.xmux.hMaxRequestTimesMax); + addMuxRange("hMaxReusableSecs", xhttp.xmux.hMaxReusableSecsMin, xhttp.xmux.hMaxReusableSecsMax); if (!xhttp.xmux.hKeepAlivePeriod.isEmpty()) - { - muxObj["hKeepAlivePeriod"] = xhttp.xmux.hKeepAlivePeriod.toInt(); - } - - xhttpObj["xmux"] = muxObj; + mux[QStringLiteral("hKeepAlivePeriod")] = xhttp.xmux.hKeepAlivePeriod.toLongLong(); + if (!mux.isEmpty()) + xo[QStringLiteral("xmux")] = mux; } - streamSettings["xhttpSettings"] = xhttpObj; + streamSettings[QStringLiteral("xhttpSettings")] = xo; } - // mKCP transport settings - if (srv.transport == "mkcp") { + if (srv.transport == QLatin1String("mkcp")) { QJsonObject kcpObj; - if (!mkcp.tti.isEmpty()) - { - kcpObj["tti"] = mkcp.tti.toInt(); - } - if (!mkcp.uplinkCapacity.isEmpty()) - { - kcpObj["uplinkCapacity"] = mkcp.uplinkCapacity.toInt(); - } - if (!mkcp.downlinkCapacity.isEmpty()) - { - kcpObj["downlinkCapacity"] = mkcp.downlinkCapacity.toInt(); - } - if (!mkcp.readBufferSize.isEmpty()) - { - kcpObj["readBufferSize"] = mkcp.readBufferSize.toInt(); - } - if (!mkcp.writeBufferSize.isEmpty()) - { - kcpObj["writeBufferSize"] = mkcp.writeBufferSize.toInt(); - } - kcpObj["congestion"] = mkcp.congestion; - streamSettings["kcpSettings"] = kcpObj; + const QString ttiEff = mkcp.tti.isEmpty() ? QString::fromLatin1(px::defaultMkcpTti) : mkcp.tti; + const QString upEff = mkcp.uplinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpUplinkCapacity) + : mkcp.uplinkCapacity; + const QString downEff = mkcp.downlinkCapacity.isEmpty() ? QString::fromLatin1(px::defaultMkcpDownlinkCapacity) + : mkcp.downlinkCapacity; + const QString rbufEff = mkcp.readBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpReadBufferSize) + : mkcp.readBufferSize; + const QString wbufEff = mkcp.writeBufferSize.isEmpty() ? QString::fromLatin1(px::defaultMkcpWriteBufferSize) + : mkcp.writeBufferSize; + kcpObj[QStringLiteral("tti")] = ttiEff.toInt(); + kcpObj[QStringLiteral("uplinkCapacity")] = upEff.toInt(); + kcpObj[QStringLiteral("downlinkCapacity")] = downEff.toInt(); + kcpObj[QStringLiteral("readBufferSize")] = rbufEff.toInt(); + kcpObj[QStringLiteral("writeBufferSize")] = wbufEff.toInt(); + kcpObj[QStringLiteral("congestion")] = mkcp.congestion; + streamSettings[QStringLiteral("kcpSettings")] = kcpObj; } return streamSettings; @@ -446,7 +497,7 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia // Build full client config QJsonObject inboundObj; - inboundObj["listen"] = amnezia::protocols::xray::defaultLocalAddr; + inboundObj["listen"] = amnezia::protocols::xray::defaultLocalListenAddr; inboundObj[amnezia::protocols::xray::port] = amnezia::protocols::xray::defaultLocalProxyPort; inboundObj["protocol"] = "socks"; inboundObj[amnezia::protocols::xray::settings] = QJsonObject { { "udp", true } }; diff --git a/client/core/configurators/xrayConfigurator.h b/client/core/configurators/xrayConfigurator.h index fe1c84cbd..968e85b44 100644 --- a/client/core/configurators/xrayConfigurator.h +++ b/client/core/configurators/xrayConfigurator.h @@ -20,6 +20,9 @@ public: const amnezia::DnsSettings &dnsSettings, amnezia::ErrorCode &errorCode) override; + amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings, + amnezia::ProtocolConfig protocolConfig) override; + private: QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig, const amnezia::DnsSettings &dnsSettings, diff --git a/client/core/installers/xrayInstaller.cpp b/client/core/installers/xrayInstaller.cpp index 47ddf52a9..30e61cc2a 100644 --- a/client/core/installers/xrayInstaller.cpp +++ b/client/core/installers/xrayInstaller.cpp @@ -14,8 +14,18 @@ #include "core/models/protocols/xrayProtocolConfig.h" #include "logger.h" -namespace { +namespace +{ Logger logger("XrayInstaller"); + + // Xray expects uTLS preset names (chrome, firefox, …). Old Amnezia/server templates used "Mozilla/5.0". + QString normalizeXrayFingerprint(const QString &fp) + { + if (fp.isEmpty() || fp.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + return QString::fromLatin1(protocols::xray::defaultFingerprint); + } + return fp; + } } using namespace amnezia; @@ -103,14 +113,14 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c srv.site = srv.sni; } - srv.fingerprint = rs.value(protocols::xray::fingerprint).toString("Mozilla/5.0"); + srv.fingerprint = normalizeXrayFingerprint(rs.value(protocols::xray::fingerprint).toString()); } // ── TLS settings ────────────────────────────────────────────────── if (srv.security == "tls") { QJsonObject tls = streamSettings.value("tlsSettings").toObject(); srv.sni = tls.value(protocols::xray::serverName).toString(); - srv.fingerprint = tls.value(protocols::xray::fingerprint).toString("Mozilla/5.0"); + srv.fingerprint = normalizeXrayFingerprint(tls.value(protocols::xray::fingerprint).toString()); QJsonArray alpnArr = tls.value("alpn").toArray(); QStringList alpnList; @@ -129,29 +139,94 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c } } - // ── XHTTP settings ──────────────────────────────────────────────── + // ── XHTTP settings (Xray-core SplitHTTPConfig + legacy Amnezia keys) ── if (srv.transport == "xhttp") { QJsonObject xhttpObj = streamSettings.value("xhttpSettings").toObject(); - srv.xhttp.mode = xhttpObj.value("mode").toString("Auto"); + { + const QString m = xhttpObj.value("mode").toString(); + if (m.isEmpty() || m == QLatin1String("auto")) + srv.xhttp.mode = QStringLiteral("Auto"); + else if (m == QLatin1String("packet-up")) + srv.xhttp.mode = QStringLiteral("Packet-up"); + else if (m == QLatin1String("stream-up")) + srv.xhttp.mode = QStringLiteral("Stream-up"); + else if (m == QLatin1String("stream-one")) + srv.xhttp.mode = QStringLiteral("Stream-one"); + else + srv.xhttp.mode = m; + } + srv.xhttp.host = xhttpObj.value("host").toString(); srv.xhttp.path = xhttpObj.value("path").toString(); - srv.xhttp.uplinkMethod = xhttpObj.value("method").toString("POST"); + + { + const QJsonObject hdrs = xhttpObj.value("headers").toObject(); + if (hdrs.contains(QLatin1String("Host")) || !hdrs.isEmpty()) + srv.xhttp.headersTemplate = QStringLiteral("HTTP"); + } + + if (xhttpObj.contains(QLatin1String("uplinkHTTPMethod"))) + srv.xhttp.uplinkMethod = xhttpObj.value("uplinkHTTPMethod").toString(); + else + srv.xhttp.uplinkMethod = xhttpObj.value("method").toString(); + srv.xhttp.disableGrpc = xhttpObj.value("noGRPCHeader").toBool(true); srv.xhttp.disableSse = xhttpObj.value("noSSEHeader").toBool(true); - srv.xhttp.sessionPlacement = xhttpObj.value("scSessionPlacement").toString("Path"); - srv.xhttp.seqPlacement = xhttpObj.value("scSeqPlacement").toString("Path"); - srv.xhttp.uplinkDataPlacement = xhttpObj.value("scUplinkDataPlacement").toString("Body"); + auto sessionSeqUi = [](const QString &core) -> QString { + if (core.isEmpty() || core == QLatin1String("path")) + return QStringLiteral("Path"); + if (core == QLatin1String("cookie")) + return QStringLiteral("Cookie"); + if (core == QLatin1String("header")) + return QStringLiteral("Header"); + if (core == QLatin1String("query")) + return QStringLiteral("Query"); + return core; + }; + QString sess = xhttpObj.value("sessionPlacement").toString(); + if (sess.isEmpty()) + sess = xhttpObj.value("scSessionPlacement").toString(); + srv.xhttp.sessionPlacement = sessionSeqUi(sess); - if (xhttpObj.contains("xhttpUplinkChunkSize")) { - srv.xhttp.uplinkChunkSize = QString::number(xhttpObj["xhttpUplinkChunkSize"].toInt()); + QString seq = xhttpObj.value("seqPlacement").toString(); + if (seq.isEmpty()) + seq = xhttpObj.value("scSeqPlacement").toString(); + srv.xhttp.seqPlacement = sessionSeqUi(seq); + + auto uplinkDataUi = [](const QString &core) -> QString { + if (core.isEmpty() || core == QLatin1String("body")) + return QStringLiteral("Body"); + if (core == QLatin1String("auto")) + return QStringLiteral("Auto"); + if (core == QLatin1String("header")) + return QStringLiteral("Header"); + if (core == QLatin1String("cookie")) + return QStringLiteral("Cookie"); + return core; + }; + QString udata = xhttpObj.value("uplinkDataPlacement").toString(); + if (udata.isEmpty()) + udata = xhttpObj.value("scUplinkDataPlacement").toString(); + srv.xhttp.uplinkDataPlacement = uplinkDataUi(udata); + + srv.xhttp.sessionKey = xhttpObj.value("sessionKey").toString(); + srv.xhttp.seqKey = xhttpObj.value("seqKey").toString(); + srv.xhttp.uplinkDataKey = xhttpObj.value("uplinkDataKey").toString(); + + if (xhttpObj.contains(QLatin1String("uplinkChunkSize"))) { + QJsonObject uc = xhttpObj.value("uplinkChunkSize").toObject(); + if (!uc.isEmpty()) + srv.xhttp.uplinkChunkSize = QString::number(uc.value("from").toInt()); + } else if (xhttpObj.contains(QLatin1String("xhttpUplinkChunkSize"))) { + srv.xhttp.uplinkChunkSize = QString::number(xhttpObj.value("xhttpUplinkChunkSize").toInt()); } - if (xhttpObj.contains("scMaxBufferedPosts")) { - srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj["scMaxBufferedPosts"].toInt()); + if (xhttpObj.contains(QLatin1String("scMaxBufferedPosts"))) { + srv.xhttp.scMaxBufferedPosts = QString::number(xhttpObj.value("scMaxBufferedPosts").toVariant().toLongLong()); } auto readRange = [&](const char *key, QString &minOut, QString &maxOut) { - QJsonObject r = xhttpObj.value(key).toObject(); + QJsonObject r = xhttpObj.value(QLatin1String(key)).toObject(); if (!r.isEmpty()) { minOut = QString::number(r.value("from").toInt()); maxOut = QString::number(r.value("to").toInt()); @@ -161,28 +236,51 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c readRange("scMinPostsIntervalMs", srv.xhttp.scMinPostsIntervalMsMin, srv.xhttp.scMinPostsIntervalMsMax); readRange("scStreamUpServerSecs", srv.xhttp.scStreamUpServerSecsMin, srv.xhttp.scStreamUpServerSecsMax); - // xPadding - if (xhttpObj.contains("xPadding")) { - QJsonObject pad = xhttpObj["xPadding"].toObject(); - srv.xhttp.xPadding.obfsMode = true; + auto loadPaddingFromObject = [&](const QJsonObject &pad) { + if (pad.contains(QLatin1String("xPaddingObfsMode"))) + srv.xhttp.xPadding.obfsMode = pad.value("xPaddingObfsMode").toBool(true); srv.xhttp.xPadding.key = pad.value("xPaddingKey").toString(); srv.xhttp.xPadding.header = pad.value("xPaddingHeader").toString(); - srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString("Cookie"); - srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString("Repeat-x"); + srv.xhttp.xPadding.placement = pad.value("xPaddingPlacement").toString(); + srv.xhttp.xPadding.method = pad.value("xPaddingMethod").toString(); QJsonObject bytesRange = pad.value("xPaddingBytes").toObject(); if (!bytesRange.isEmpty()) { srv.xhttp.xPadding.bytesMin = QString::number(bytesRange.value("from").toInt()); srv.xhttp.xPadding.bytesMax = QString::number(bytesRange.value("to").toInt()); } + QString pl = srv.xhttp.xPadding.placement.toLower(); + if (pl == QLatin1String("cookie")) + srv.xhttp.xPadding.placement = QStringLiteral("Cookie"); + else if (pl == QLatin1String("header")) + srv.xhttp.xPadding.placement = QStringLiteral("Header"); + else if (pl == QLatin1String("query")) + srv.xhttp.xPadding.placement = QStringLiteral("Query"); + else if (pl == QLatin1String("queryinheader")) + srv.xhttp.xPadding.placement = QStringLiteral("Query in header"); + QString met = srv.xhttp.xPadding.method.toLower(); + if (met == QLatin1String("repeat-x")) + srv.xhttp.xPadding.method = QStringLiteral("Repeat-x"); + else if (met == QLatin1String("tokenish")) + srv.xhttp.xPadding.method = QStringLiteral("Tokenish"); + }; + if (xhttpObj.contains(QLatin1String("xPaddingObfsMode")) || xhttpObj.contains(QLatin1String("xPaddingKey")) + || !xhttpObj.value("xPaddingBytes").toObject().isEmpty()) { + loadPaddingFromObject(xhttpObj); + } else if (xhttpObj.contains(QLatin1String("xPadding")) && xhttpObj.value("xPadding").isObject()) { + const QJsonObject nested = xhttpObj.value("xPadding").toObject(); + if (!nested.isEmpty()) { + loadPaddingFromObject(nested); + if (!nested.contains(QLatin1String("xPaddingObfsMode"))) + srv.xhttp.xPadding.obfsMode = true; + } } - // xmux - if (xhttpObj.contains("xmux")) { - QJsonObject mux = xhttpObj["xmux"].toObject(); - srv.xhttp.xmux.enabled = mux.value("enabled").toBool(true); + if (xhttpObj.contains(QLatin1String("xmux"))) { + QJsonObject mux = xhttpObj.value("xmux").toObject(); + srv.xhttp.xmux.enabled = true; auto readMuxRange = [&](const char *key, QString &minOut, QString &maxOut) { - QJsonObject r = mux.value(key).toObject(); + QJsonObject r = mux.value(QLatin1String(key)).toObject(); if (!r.isEmpty()) { minOut = QString::number(r.value("from").toInt()); maxOut = QString::number(r.value("to").toInt()); @@ -194,8 +292,8 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c readMuxRange("hMaxRequestTimes", srv.xhttp.xmux.hMaxRequestTimesMin, srv.xhttp.xmux.hMaxRequestTimesMax); readMuxRange("hMaxReusableSecs", srv.xhttp.xmux.hMaxReusableSecsMin, srv.xhttp.xmux.hMaxReusableSecsMax); - if (mux.contains("hKeepAlivePeriod")) - srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux["hKeepAlivePeriod"].toInt()); + if (mux.contains(QLatin1String("hKeepAlivePeriod"))) + srv.xhttp.xmux.hKeepAlivePeriod = QString::number(mux.value("hKeepAlivePeriod").toVariant().toLongLong()); } } diff --git a/client/core/models/protocols/xrayProtocolConfig.cpp b/client/core/models/protocols/xrayProtocolConfig.cpp index be3edee3b..194559e90 100644 --- a/client/core/models/protocols/xrayProtocolConfig.cpp +++ b/client/core/models/protocols/xrayProtocolConfig.cpp @@ -13,11 +13,6 @@ using namespace ProtocolUtils; namespace amnezia { - -// ═════════════════════════════════════════════════════════════════════════════ -// XrayXPaddingConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayXPaddingConfig::toJson() const { QJsonObject obj; @@ -37,17 +32,13 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json) c.bytesMin = json.value(configKey::xPaddingBytesMin).toString(); c.bytesMax = json.value(configKey::xPaddingBytesMax).toString(); c.obfsMode = json.value(configKey::xPaddingObfsMode).toBool(true); - c.key = json.value(configKey::xPaddingKey).toString("www.googletagmanager.com"); + c.key = json.value(configKey::xPaddingKey).toString(protocols::xray::defaultSite); c.header = json.value(configKey::xPaddingHeader).toString(); - c.placement = json.value(configKey::xPaddingPlacement).toString("Cookie"); - c.method = json.value(configKey::xPaddingMethod).toString("Repeat-x"); + c.placement = json.value(configKey::xPaddingPlacement).toString(protocols::xray::defaultXPaddingPlacement); + c.method = json.value(configKey::xPaddingMethod).toString(protocols::xray::defaultXPaddingMethod); return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayXmuxConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayXmuxConfig::toJson() const { QJsonObject obj; @@ -84,10 +75,6 @@ XrayXmuxConfig XrayXmuxConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayXhttpConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayXhttpConfig::toJson() const { QJsonObject obj; @@ -124,19 +111,19 @@ QJsonObject XrayXhttpConfig::toJson() const XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json) { XrayXhttpConfig c; - c.mode = json.value(configKey::xhttpMode).toString("Auto"); - c.host = json.value(configKey::xhttpHost).toString("www.googletagmanager.com"); + c.mode = json.value(configKey::xhttpMode).toString(protocols::xray::defaultXhttpMode); + c.host = json.value(configKey::xhttpHost).toString(protocols::xray::defaultSite); c.path = json.value(configKey::xhttpPath).toString(); - c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString("HTTP"); - c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString("POST"); + c.headersTemplate = json.value(configKey::xhttpHeadersTemplate).toString(protocols::xray::defaultXhttpHeadersTemplate); + c.uplinkMethod = json.value(configKey::xhttpUplinkMethod).toString(protocols::xray::defaultXhttpUplinkMethod); c.disableGrpc = json.value(configKey::xhttpDisableGrpc).toBool(true); c.disableSse = json.value(configKey::xhttpDisableSse).toBool(true); - c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString("Path"); - c.sessionKey = json.value(configKey::xhttpSessionKey).toString("Path"); - c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString("Path"); + c.sessionPlacement = json.value(configKey::xhttpSessionPlacement).toString(protocols::xray::defaultXhttpSessionPlacement); + c.sessionKey = json.value(configKey::xhttpSessionKey).toString(); + c.seqPlacement = json.value(configKey::xhttpSeqPlacement).toString(protocols::xray::defaultXhttpSessionPlacement); c.seqKey = json.value(configKey::xhttpSeqKey).toString(); - c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString("Body"); + c.uplinkDataPlacement = json.value(configKey::xhttpUplinkDataPlacement).toString(protocols::xray::defaultXhttpUplinkDataPlacement); c.uplinkDataKey = json.value(configKey::xhttpUplinkDataKey).toString(); c.uplinkChunkSize = json.value(configKey::xhttpUplinkChunkSize).toString("0"); @@ -154,10 +141,6 @@ XrayXhttpConfig XrayXhttpConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayMkcpConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayMkcpConfig::toJson() const { QJsonObject obj; @@ -182,10 +165,6 @@ XrayMkcpConfig XrayMkcpConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayServerConfig -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayServerConfig::toJson() const { QJsonObject obj; @@ -224,14 +203,17 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json) c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); // New: Security - c.security = json.value(configKey::xraySecurity).toString("reality"); - c.flow = json.value(configKey::xrayFlow).toString("xtls-rprx-vision"); - c.fingerprint = json.value(configKey::xrayFingerprint).toString("Mozilla/5.0"); - c.sni = json.value(configKey::xraySni).toString("cdn.example.com"); - c.alpn = json.value(configKey::xrayAlpn).toString("HTTP/2"); + c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity); + c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow); + c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint); + if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); + } + c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni); + c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn); // New: Transport - c.transport = json.value(configKey::xrayTransport).toString("raw"); + c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport); c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject()); c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject()); @@ -249,10 +231,6 @@ bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) con && sni == other.sni; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayClientConfig (unchanged logic, kept as-is) -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayClientConfig::toJson() const { QJsonObject obj; @@ -300,10 +278,6 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json) return c; } -// ═════════════════════════════════════════════════════════════════════════════ -// XrayProtocolConfig (unchanged logic, kept as-is) -// ═════════════════════════════════════════════════════════════════════════════ - QJsonObject XrayProtocolConfig::toJson() const { QJsonObject obj = serverConfig.toJson(); diff --git a/client/core/models/protocols/xrayProtocolConfig.h b/client/core/models/protocols/xrayProtocolConfig.h index a4ae2645f..73c160bc7 100644 --- a/client/core/models/protocols/xrayProtocolConfig.h +++ b/client/core/models/protocols/xrayProtocolConfig.h @@ -2,6 +2,7 @@ #define XRAYPROTOCOLCONFIG_H #include +#include "core/utils/constants/protocolConstants.h" #include #include @@ -15,8 +16,8 @@ struct XrayXPaddingConfig { bool obfsMode = true; // xPaddingObfsMode QString key; // xPaddingKey QString header; // xPaddingHeader - QString placement = "Cookie"; // xPaddingPlacement: Cookie|Header|Query|Body - QString method = "Repeat-x"; // xPaddingMethod: Repeat-x|Random|Zero + QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body + QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero QJsonObject toJson() const; static XrayXPaddingConfig fromJson(const QJsonObject &json); @@ -44,31 +45,31 @@ struct XrayXmuxConfig { // ── XHTTP transport ─────────────────────────────────────────────────────────── struct XrayXhttpConfig { - QString mode = "Auto"; // Auto|Packet-up|Stream-up|Stream-one - QString host = "www.googletagmanager.com"; + QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one + QString host = protocols::xray::defaultXhttpHost; QString path; - QString headersTemplate = "HTTP"; // HTTP|None - QString uplinkMethod = "POST"; // POST|PUT|PATCH + QString headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; // HTTP|None + QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; // POST|PUT|PATCH bool disableGrpc = true; bool disableSse = true; // Session & Sequence - QString sessionPlacement = "Path"; // Path|Header|Cookie|None - QString sessionKey = "Path"; - QString seqPlacement = "Path"; + QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement; + QString sessionKey = protocols::xray::defaultXhttpSessionKey; + QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement; QString seqKey; - QString uplinkDataPlacement = "Body"; // Body|Query + QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement; QString uplinkDataKey; // Traffic Shaping - QString uplinkChunkSize = "0"; + QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize; QString scMaxBufferedPosts; - QString scMaxEachPostBytesMin = "1"; - QString scMaxEachPostBytesMax = "100"; - QString scMinPostsIntervalMsMin = "100"; - QString scMinPostsIntervalMsMax = "800"; - QString scStreamUpServerSecsMin = "1"; - QString scStreamUpServerSecsMax = "100"; + QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin; + QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax; + QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin; + QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax; + QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin; + QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax; XrayXPaddingConfig xPadding; XrayXmuxConfig xmux; @@ -100,14 +101,14 @@ struct XrayServerConfig { bool isThirdPartyConfig = false; // New: Security - QString security = "reality"; // none|tls|reality - QString flow = "xtls-rprx-vision"; // ""|xtls-rprx-vision|xtls-rprx-vision-udp443 - QString fingerprint = "Mozilla/5.0"; - QString sni = "cdn.example.com"; - QString alpn = "HTTP/2"; // TLS only: HTTP/2|HTTP/1.1|HTTP/2,HTTP/1.1 + QString security = protocols::xray::defaultSecurity; + QString flow = protocols::xray::defaultFlow; + QString fingerprint = protocols::xray::defaultFingerprint; + QString sni = protocols::xray::defaultSni; + QString alpn = protocols::xray::defaultAlpn; // New: Transport - QString transport = "raw"; // raw|xhttp|mkcp + QString transport = protocols::xray::defaultTransport; XrayXhttpConfig xhttp; XrayMkcpConfig mkcp; diff --git a/client/core/protocols/xrayProtocol.cpp b/client/core/protocols/xrayProtocol.cpp old mode 100755 new mode 100644 index 893b8367e..ffa47361a --- a/client/core/protocols/xrayProtocol.cpp +++ b/client/core/protocols/xrayProtocol.cpp @@ -2,6 +2,7 @@ #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" #include "core/utils/ipcClient.h" #include "core/utils/networkUtilities.h" #include "core/utils/serialization/serialization.h" @@ -9,6 +10,7 @@ #include #include +#include #include #include #include @@ -79,12 +81,29 @@ ErrorCode XrayProtocol::start() m_socksPassword = creds.password; m_socksPort = creds.port; - const QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact); + QString xrayConfigStr = QJsonDocument(m_xrayConfig).toJson(QJsonDocument::Compact); if (xrayConfigStr.isEmpty()) { qCritical() << "Xray config is empty"; return ErrorCode::XrayExecutableCrashed; } + // Fix fingerprint: old configs may contain "Mozilla/5.0" which xray-core rejects. + // Replace with the correct default at runtime so stale stored configs still work. + if (xrayConfigStr.contains("Mozilla/5.0", Qt::CaseInsensitive)) { + xrayConfigStr.replace("Mozilla/5.0", amnezia::protocols::xray::defaultFingerprint, + Qt::CaseInsensitive); + qDebug() << "XrayProtocol: patched legacy fingerprint to" + << amnezia::protocols::xray::defaultFingerprint; + } + + // Fix inbound listen address: old configs may use "10.33.0.2" which doesn't exist + // until TUN is created. xray must listen on 127.0.0.1 so tun2socks can connect. + if (xrayConfigStr.contains(amnezia::protocols::xray::defaultLocalAddr)) { + xrayConfigStr.replace(amnezia::protocols::xray::defaultLocalAddr, + amnezia::protocols::xray::defaultLocalListenAddr); + qDebug() << "XrayProtocol: patched legacy inbound listen address to 127.0.0.1"; + } + return IpcClient::withInterface( [&](QSharedPointer iface) { auto xrayStart = iface->xrayStart(xrayConfigStr); @@ -188,6 +207,33 @@ ErrorCode XrayProtocol::startTun2Socks() connect( m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + // Check stdout for "resource busy" — the TUN device was not yet released + // by the previous tun2socks instance. Retry after a short delay. + bool resourceBusy = false; + if (m_tun2socksProcess) { + auto readOut = m_tun2socksProcess->readAllStandardOutput(); + if (readOut.waitForFinished()) { + resourceBusy = readOut.returnValue().contains("resource busy"); + } + } + + if (resourceBusy && m_tun2socksRetryCount < maxTun2SocksRetries) { + m_tun2socksRetryCount++; + qWarning() << QString("Tun2socks: TUN resource busy, retrying (%1/%2) in %3ms...") + .arg(m_tun2socksRetryCount) + .arg(maxTun2SocksRetries) + .arg(tun2socksRetryDelayMs); + QTimer::singleShot(tun2socksRetryDelayMs, this, [this]() { + if (ErrorCode err = startTun2Socks(); err != ErrorCode::NoError) { + stop(); + setLastError(err); + } + }); + return; + } + + m_tun2socksRetryCount = 0; + if (exitStatus == QProcess::ExitStatus::CrashExit) { qCritical() << "Tun2socks process crashed!"; } else { diff --git a/client/core/protocols/xrayProtocol.h b/client/core/protocols/xrayProtocol.h index e831ab2f4..55b6d1d5c 100644 --- a/client/core/protocols/xrayProtocol.h +++ b/client/core/protocols/xrayProtocol.h @@ -35,6 +35,9 @@ private: int m_socksPort = 10808; QSharedPointer m_tun2socksProcess; + int m_tun2socksRetryCount = 0; + static constexpr int maxTun2SocksRetries = 5; + static constexpr int tun2socksRetryDelayMs = 400; }; #endif // XRAYPROTOCOL_H diff --git a/client/core/utils/constants/protocolConstants.h b/client/core/utils/constants/protocolConstants.h index 01e2a151a..3b8fd9c38 100644 --- a/client/core/utils/constants/protocolConstants.h +++ b/client/core/utils/constants/protocolConstants.h @@ -57,6 +57,40 @@ namespace amnezia constexpr char defaultPort[] = "443"; constexpr char defaultLocalProxyPort[] = "10808"; constexpr char defaultLocalAddr[] = "10.33.0.2"; + constexpr char defaultLocalListenAddr[] = "127.0.0.1"; + + constexpr char defaultSecurity[] = "reality"; + constexpr char defaultFlow[] = "xtls-rprx-vision"; + constexpr char defaultTransport[] = "raw"; + constexpr char defaultFingerprint[] = "chrome"; + constexpr char defaultSni[] = "cdn.example.com"; + constexpr char defaultAlpn[] = "HTTP/2"; + + constexpr char defaultXhttpMode[] = "Auto"; + constexpr char defaultXhttpHeadersTemplate[] = "HTTP"; + constexpr char defaultXhttpUplinkMethod[] = "POST"; + constexpr char defaultXhttpSessionPlacement[] = "Path"; + constexpr char defaultXhttpSessionKey[] = "Path"; + constexpr char defaultXhttpSeqPlacement[] = "Path"; + constexpr char defaultXhttpUplinkDataPlacement[] = "Body"; + + constexpr char defaultXhttpHost[] = "www.googletagmanager.com"; + constexpr char defaultXhttpUplinkChunkSize[] = "0"; + constexpr char defaultXhttpScMaxEachPostBytesMin[] = "1"; + constexpr char defaultXhttpScMaxEachPostBytesMax[] = "100"; + constexpr char defaultXhttpScMinPostsIntervalMsMin[] = "100"; + constexpr char defaultXhttpScMinPostsIntervalMsMax[] = "800"; + constexpr char defaultXhttpScStreamUpServerSecsMin[] = "1"; + constexpr char defaultXhttpScStreamUpServerSecsMax[] = "100"; + + constexpr char defaultXPaddingPlacement[] = "Cookie"; + constexpr char defaultXPaddingMethod[] = "Repeat-x"; + + constexpr char defaultMkcpTti[] = "50"; + constexpr char defaultMkcpUplinkCapacity[] = "5"; + constexpr char defaultMkcpDownlinkCapacity[] = "20"; + constexpr char defaultMkcpReadBufferSize[] = "2"; + constexpr char defaultMkcpWriteBufferSize[] = "2"; constexpr char outbounds[] = "outbounds"; constexpr char inbounds[] = "inbounds"; diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index d8a7e8d4f..6e215afcf 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -263,31 +263,75 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, endResetModel(); } -void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig& config) +void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config) { - if (config.port.isEmpty()) + if (config.port.isEmpty()) { config.port = protocols::xray::defaultPort; + } - if (config.site.isEmpty()) + if (config.site.isEmpty()) { config.site = protocols::xray::defaultSite; + } - if (config.transport.isEmpty()) - config.transport = "raw"; + if (config.transport.isEmpty()) { + config.transport = protocols::xray::defaultTransport; + } - if (config.security.isEmpty()) - config.security = "reality"; + if (config.security.isEmpty()) { + config.security = protocols::xray::defaultSecurity; + } - if (config.flow.isEmpty()) - config.flow = "xtls-rprx-vision"; + if (config.flow.isEmpty()) { + config.flow = protocols::xray::defaultFlow; + } - if (config.fingerprint.isEmpty()) - config.fingerprint = "Mozilla/5.0"; + if (config.fingerprint.isEmpty()) { + config.fingerprint = protocols::xray::defaultFingerprint; + } else if (config.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { + config.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); + } - if (config.sni.isEmpty()) - config.sni = "cdn.example.com"; + if (config.sni.isEmpty()) { + config.sni = protocols::xray::defaultSni; + } - if (config.alpn.isEmpty()) - config.alpn = "HTTP/2"; + if (config.alpn.isEmpty()) { + config.alpn = protocols::xray::defaultAlpn; + } + + // XHTTP transport defaults + if (config.xhttp.host.isEmpty()) { + config.xhttp.host = protocols::xray::defaultXhttpHost; + } + if (config.xhttp.mode.isEmpty()) { + config.xhttp.mode = protocols::xray::defaultXhttpMode; + } + if (config.xhttp.headersTemplate.isEmpty()) { + config.xhttp.headersTemplate = protocols::xray::defaultXhttpHeadersTemplate; + } + if (config.xhttp.uplinkMethod.isEmpty()) { + config.xhttp.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod; + } + if (config.xhttp.sessionPlacement.isEmpty()) { + config.xhttp.sessionPlacement = protocols::xray::defaultXhttpSessionPlacement; + } + if (config.xhttp.sessionKey.isEmpty()) { + config.xhttp.sessionKey = protocols::xray::defaultXhttpSessionKey; + } + if (config.xhttp.seqPlacement.isEmpty()) { + config.xhttp.seqPlacement = protocols::xray::defaultXhttpSeqPlacement; + } + if (config.xhttp.uplinkDataPlacement.isEmpty()) { + config.xhttp.uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement; + } + + // xPadding defaults + if (config.xhttp.xPadding.placement.isEmpty()) { + config.xhttp.xPadding.placement = protocols::xray::defaultXPaddingPlacement; + } + if (config.xhttp.xPadding.method.isEmpty()) { + config.xhttp.xPadding.method = protocols::xray::defaultXPaddingMethod; + } } amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig() @@ -396,3 +440,104 @@ void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverC m_originalProtocolConfig = m_protocolConfig; endResetModel(); } + +QStringList XrayConfigModel::flowOptions() +{ + return { + "", // Empty (no flow) + "xtls-rprx-vision", + "xtls-rprx-vision-udp443" + }; +} + +QStringList XrayConfigModel::securityOptions() +{ + return { "none", "tls", "reality" }; +} + +QStringList XrayConfigModel::transportOptions() +{ + return { "raw", "xhttp", "mkcp" }; +} + +QStringList XrayConfigModel::fingerprintOptions() +{ + return { "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random" }; +} + +QStringList XrayConfigModel::alpnOptions() +{ + return { "HTTP/2", "HTTP/1.1", "HTTP/2,HTTP/1.1" }; +} + +QStringList XrayConfigModel::xhttpModeOptions() +{ + return { "Auto", "Packet-up", "Stream-up", "Stream-one" }; +} + +QStringList XrayConfigModel::xhttpHeadersTemplateOptions() +{ + return { "HTTP", "None" }; +} + +QStringList XrayConfigModel::xhttpUplinkMethodOptions() +{ + return { "POST", "PUT", "PATCH" }; +} + +QStringList XrayConfigModel::xhttpSessionPlacementOptions() +{ + return { "Path", "Header", "Cookie", "None" }; +} + +QStringList XrayConfigModel::xhttpSessionKeyOptions() +{ + return { "Path", "Header", "None" }; +} + +QStringList XrayConfigModel::xhttpSeqPlacementOptions() +{ + return { "Path", "Header", "Cookie", "None" }; +} + +QStringList XrayConfigModel::xhttpUplinkDataPlacementOptions() +{ + // Matches splithttp uplink payload placement (packet-up / advanced) + return { "Body", "Auto", "Header", "Cookie" }; +} + +QStringList XrayConfigModel::xPaddingPlacementOptions() +{ + // Xray-core: cookie | header | query | queryInHeader (not "body") + return { "Cookie", "Header", "Query", "Query in header" }; +} + +QStringList XrayConfigModel::xPaddingMethodOptions() +{ + return { "Repeat-x", "Tokenish" }; +} + +QString XrayConfigModel::mkcpDefaultTti() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpTti); +} + +QString XrayConfigModel::mkcpDefaultUplinkCapacity() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpUplinkCapacity); +} + +QString XrayConfigModel::mkcpDefaultDownlinkCapacity() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpDownlinkCapacity); +} + +QString XrayConfigModel::mkcpDefaultReadBufferSize() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpReadBufferSize); +} + +QString XrayConfigModel::mkcpDefaultWriteBufferSize() +{ + return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize); +} diff --git a/client/ui/models/protocols/xrayConfigModel.h b/client/ui/models/protocols/xrayConfigModel.h index ac41f228c..c317e955a 100644 --- a/client/ui/models/protocols/xrayConfigModel.h +++ b/client/ui/models/protocols/xrayConfigModel.h @@ -2,6 +2,7 @@ #define XRAYCONFIGMODEL_H #include +#include #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" @@ -93,6 +94,29 @@ public: bool setData(const QModelIndex& index, const QVariant& value, int role) override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + // ── Static option lists (for QML DropDown models) ───────────────── + Q_INVOKABLE static QStringList flowOptions(); + Q_INVOKABLE static QStringList securityOptions(); + Q_INVOKABLE static QStringList transportOptions(); + Q_INVOKABLE static QStringList fingerprintOptions(); + Q_INVOKABLE static QStringList alpnOptions(); + Q_INVOKABLE static QStringList xhttpModeOptions(); + Q_INVOKABLE static QStringList xhttpHeadersTemplateOptions(); + Q_INVOKABLE static QStringList xhttpUplinkMethodOptions(); + Q_INVOKABLE static QStringList xhttpSessionPlacementOptions(); + Q_INVOKABLE static QStringList xhttpSessionKeyOptions(); + Q_INVOKABLE static QStringList xhttpSeqPlacementOptions(); + Q_INVOKABLE static QStringList xhttpUplinkDataPlacementOptions(); + Q_INVOKABLE static QStringList xPaddingPlacementOptions(); + Q_INVOKABLE static QStringList xPaddingMethodOptions(); + + // mKCP display defaults (protocolConstants.h — must match xrayConfigurator empty-field behavior) + Q_INVOKABLE static QString mkcpDefaultTti(); + Q_INVOKABLE static QString mkcpDefaultUplinkCapacity(); + Q_INVOKABLE static QString mkcpDefaultDownlinkCapacity(); + Q_INVOKABLE static QString mkcpDefaultReadBufferSize(); + Q_INVOKABLE static QString mkcpDefaultWriteBufferSize(); + public slots: void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig); amnezia::XrayProtocolConfig getProtocolConfig(); diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 897584303..c18755625 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -10,6 +10,7 @@ Item { id: root property string headerText + property string subtitleText // optional line under header (e.g. default value hint) property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray property string headerTextColor: AmneziaStyle.color.mutedGray @@ -84,6 +85,15 @@ Item { Layout.fillWidth: true } + SmallTextType { + text: root.subtitleText + visible: root.subtitleText !== "" + color: AmneziaStyle.color.charcoalGray + font.pixelSize: 13 + Layout.fillWidth: true + Layout.topMargin: visible ? 2 : 0 + } + TextField { id: textField diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml index 6617b239f..53ff9be53 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -86,20 +88,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } } diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml index a1dda29e4..0ef419d50 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -99,14 +101,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "HTTP/2" - } - ListElement { - name: "HTTP/1.1" - } - ListElement { - name: "HTTP/2,HTTP/1.1" + Component.onCompleted: { + var opts = XrayConfigModel.alpnOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -145,35 +144,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Mozilla/5.0" - } - ListElement { - name: "chrome" - } - ListElement { - name: "firefox" - } - ListElement { - name: "safari" - } - ListElement { - name: "ios" - } - ListElement { - name: "android" - } - ListElement { - name: "edge" - } - ListElement { - name: "360" - } - ListElement { - name: "qq" - } - ListElement { - name: "random" + Component.onCompleted: { + var opts = XrayConfigModel.fingerprintOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -231,35 +206,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Mozilla/5.0" - } - ListElement { - name: "chrome" - } - ListElement { - name: "firefox" - } - ListElement { - name: "safari" - } - ListElement { - name: "ios" - } - ListElement { - name: "android" - } - ListElement { - name: "edge" - } - ListElement { - name: "360" - } - ListElement { - name: "qq" - } - ListElement { - name: "random" + Component.onCompleted: { + var opts = XrayConfigModel.fingerprintOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -304,21 +255,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } -} + BasicButtonType { + id: saveButton + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 59d0a6d9f..b0188b7fb 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -58,7 +58,7 @@ PageType { Header2TextType { Layout.fillWidth: true - text: qsTr("XRay\nVLESS") + text: qsTr("XRay VLESS settings") wrapMode: Text.WordWrap } diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml index 8b78eb14d..c62cd82bc 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -106,6 +108,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("TTI") + subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti()) textField.text: mkcpTti textField.onEditingFinished: { if (textField.text !== mkcpTti) mkcpTti = textField.text @@ -118,6 +121,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("uplinkCapacity") + subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity()) textField.text: mkcpUplinkCapacity textField.onEditingFinished: { if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text @@ -130,6 +134,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("downlinkCapacity") + subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity()) textField.text: mkcpDownlinkCapacity textField.onEditingFinished: { if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text @@ -142,6 +147,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("readBufferSize") + subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize()) textField.text: mkcpReadBufferSize textField.onEditingFinished: { if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text @@ -154,6 +160,7 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("writeBufferSize") + subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize()) textField.text: mkcpWriteBufferSize textField.onEditingFinished: { if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text @@ -178,10 +185,20 @@ PageType { Layout.fillWidth: true spacing: 0 + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Transport Mode") + color: AmneziaStyle.color.mutedGray + } + DropDownType { id: modeDropDown Layout.fillWidth: true - Layout.topMargin: 16 + Layout.topMargin: 0 Layout.leftMargin: 16 Layout.rightMargin: 16 text: xhttpMode @@ -191,17 +208,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Auto" - } - ListElement { - name: "Packet-up" - } - ListElement { - name: "Stream-up" - } - ListElement { - name: "Stream-one" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpModeOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -274,11 +285,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "HTTP" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpHeadersTemplateOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -317,14 +328,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "POST" - } - ListElement { - name: "PUT" - } - ListElement { - name: "PATCH" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpUplinkMethodOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -399,17 +407,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Path" - } - ListElement { - name: "Header" - } - ListElement { - name: "Cookie" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSessionPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -448,14 +450,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Path" - } - ListElement { - name: "Header" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSessionKeyOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -494,17 +493,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Path" - } - ListElement { - name: "Header" - } - ListElement { - name: "Cookie" - } - ListElement { - name: "None" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpSeqPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -555,11 +548,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Body" - } - ListElement { - name: "Query" + Component.onCompleted: { + var opts = XrayConfigModel.xhttpUplinkDataPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -654,25 +647,6 @@ PageType { onMaxChanged: xhttpScMaxEachPostBytesMax = val } - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("scMinPostsIntervalMs") - color: AmneziaStyle.color.mutedGray - } - MinMaxRowType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - minValue: xhttpScMinPostsIntervalMsMin - maxValue: xhttpScMinPostsIntervalMsMax - onMinChanged: xhttpScMinPostsIntervalMsMin = val - onMaxChanged: xhttpScMinPostsIntervalMsMax = val - } - CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 @@ -692,6 +666,25 @@ PageType { onMaxChanged: xhttpScStreamUpServerSecsMax = val } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("scMinPostsIntervalMs") + color: AmneziaStyle.color.mutedGray + } + MinMaxRowType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + minValue: xhttpScMinPostsIntervalMsMin + maxValue: xhttpScMinPostsIntervalMsMax + onMinChanged: xhttpScMinPostsIntervalMsMin = val + onMaxChanged: xhttpScMinPostsIntervalMsMax = val + } + // ── Padding and multiplexing ────────────────────────── CaptionTextType { Layout.fillWidth: true @@ -735,21 +728,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } -} + BasicButtonType { + id: saveButton + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } +} diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml index 6590ad1ed..08116eaf0 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -69,20 +71,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml index d2d0ad70d..9176eaf48 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -106,17 +108,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Cookie" - } - ListElement { - name: "Header" - } - ListElement { - name: "Query" - } - ListElement { - name: "Body" + Component.onCompleted: { + var opts = XrayConfigModel.xPaddingPlacementOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -155,14 +151,11 @@ PageType { listView: ListViewWithRadioButtonType { rootWidth: root.width model: ListModel { - ListElement { - name: "Repeat-x" - } - ListElement { - name: "Random" - } - ListElement { - name: "Zero" + Component.onCompleted: { + var opts = XrayConfigModel.xPaddingMethodOptions() + for (var i = 0; i < opts.length; i++) { + append({name: opts[i]}) + } } } clickedFunction: function () { @@ -194,20 +187,37 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } } diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml index 8376a5cc2..24793c4e0 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import PageEnum 1.0 +import ProtocolEnum 1.0 import Style 1.0 import "./" @@ -25,10 +26,11 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: parent.bottom + anchors.bottom: saveButton.top anchors.left: parent.left anchors.right: parent.right + enabled: ServersUiController.isProcessedServerHasWriteAccess() model: XrayConfigModel delegate: ColumnLayout { @@ -182,21 +184,38 @@ PageType { } } - // BasicButtonType { - // id: saveButton - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - // anchors.leftMargin: 16 - // anchors.rightMargin: 16 - // text: qsTr("Save") - // onClicked: { - // forceActiveFocus() - // PageController.closePage() - // } - // Keys.onEnterPressed: clicked() - // Keys.onReturnPressed: clicked() - // } + BasicButtonType { + id: saveButton + + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin + + enabled: listView.enabled + text: qsTr("Save") + clickedFunc: function () { + var headerText = qsTr("Save settings?") + var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { + PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) + return + } + PageController.goToPage(PageEnum.PageSetupWizardInstalling) + InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) + } + var noButtonFunction = function () { + if (typeof GC !== "undefined" && !GC.isMobile()) { + saveButton.forceActiveFocus() + } + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + } } From c48bfc0b16ac5a8ccadac21356febb0f37d900a1 Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 23 Apr 2026 14:10:42 +0300 Subject: [PATCH 09/18] add save file in temp --- service/server/xray.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/service/server/xray.cpp b/service/server/xray.cpp index 5d324014e..20345d426 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -2,6 +2,7 @@ #include "core/utils/networkUtilities.h" #include +#include #include #include #include @@ -47,6 +48,32 @@ bool Xray::startXray(const QString &cfg) amnezia_xray_setloghandler(ctxLogHandler, this); QByteArray bytes = cfg.toUtf8(); + + // Temporary aid for manual verification of SOCKS auth (inbounds.settings.accounts). + // Debug: always writes. Release: set env AMNEZIA_SAVE_XRAY_CONFIG=1 (if the daemon inherits it). + { +#if defined(MZ_DEBUG) + const bool saveForTest = true; +#else + const bool saveForTest = !qgetenv("AMNEZIA_SAVE_XRAY_CONFIG").isEmpty(); +#endif + if (saveForTest) { +#ifdef Q_OS_WIN + const QString debugPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + + QStringLiteral("/amnezia-xray-config.json"); +#else + const QString debugPath = QStringLiteral("/tmp/amnezia-xray-config.json"); +#endif + QFile f(debugPath); + if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + f.write(bytes); + f.close(); + qDebug() << "[xray] wrote last config for testing:" << debugPath; + } else { + qWarning() << "[xray] failed to write test config to" << debugPath; + } + } + } if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { qDebug() << "[xray] configuration failed: " << err; amnezia_xray_free(err); From 841d18931fb55439f9fc4a8add22f6949b62e5f7 Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 23 Apr 2026 14:14:55 +0300 Subject: [PATCH 10/18] remove debug macros --- service/server/xray.cpp | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/service/server/xray.cpp b/service/server/xray.cpp index 20345d426..fdc0099af 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -52,26 +52,19 @@ bool Xray::startXray(const QString &cfg) // Temporary aid for manual verification of SOCKS auth (inbounds.settings.accounts). // Debug: always writes. Release: set env AMNEZIA_SAVE_XRAY_CONFIG=1 (if the daemon inherits it). { -#if defined(MZ_DEBUG) - const bool saveForTest = true; -#else - const bool saveForTest = !qgetenv("AMNEZIA_SAVE_XRAY_CONFIG").isEmpty(); -#endif - if (saveForTest) { #ifdef Q_OS_WIN - const QString debugPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) - + QStringLiteral("/amnezia-xray-config.json"); + const QString debugPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + + QStringLiteral("/amnezia-xray-config.json"); #else - const QString debugPath = QStringLiteral("/tmp/amnezia-xray-config.json"); + const QString debugPath = QStringLiteral("/tmp/amnezia-xray-config.json"); #endif - QFile f(debugPath); - if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - f.write(bytes); - f.close(); - qDebug() << "[xray] wrote last config for testing:" << debugPath; - } else { - qWarning() << "[xray] failed to write test config to" << debugPath; - } + QFile f(debugPath); + if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + f.write(bytes); + f.close(); + qDebug() << "[xray] wrote last config for testing:" << debugPath; + } else { + qWarning() << "[xray] failed to write test config to" << debugPath; } } if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { From 4f83d4485610a575d515b9a5a7c759603d50850e Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 23 Apr 2026 14:58:20 +0300 Subject: [PATCH 11/18] fixed build windows --- service/server/xray.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/service/server/xray.cpp b/service/server/xray.cpp index fdc0099af..e4dee0b3c 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include From 8f84f3ce3ab4f8342d51e6151d319458874a694d Mon Sep 17 00:00:00 2001 From: dranik Date: Fri, 24 Apr 2026 09:30:49 +0300 Subject: [PATCH 12/18] fix path Windows --- service/server/xray.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/service/server/xray.cpp b/service/server/xray.cpp index e4dee0b3c..9cdf78231 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -53,12 +54,23 @@ bool Xray::startXray(const QString &cfg) // Temporary aid for manual verification of SOCKS auth (inbounds.settings.accounts). // Debug: always writes. Release: set env AMNEZIA_SAVE_XRAY_CONFIG=1 (if the daemon inherits it). { + QString debugPath; + #ifdef Q_OS_WIN - const QString debugPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) - + QStringLiteral("/amnezia-xray-config.json"); + const QString dirPath = QStringLiteral("C:/Temp"); + QDir dir(dirPath); + + if (!dir.exists()) { + if (!dir.mkpath(".")) { + qWarning() << "[xray] failed to create directory:" << dirPath; + } + } + + debugPath = dir.filePath(QStringLiteral("amnezia-xray-config.json")); #else - const QString debugPath = QStringLiteral("/tmp/amnezia-xray-config.json"); + debugPath = QStringLiteral("/tmp/amnezia-xray-config.json"); #endif + QFile f(debugPath); if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { f.write(bytes); From 28709072bfbed4f389ea03aaa63896037fb538be Mon Sep 17 00:00:00 2001 From: dranik Date: Mon, 27 Apr 2026 15:42:08 +0300 Subject: [PATCH 13/18] remove save config --- service/server/xray.cpp | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/service/server/xray.cpp b/service/server/xray.cpp index 9cdf78231..5d324014e 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -2,9 +2,6 @@ #include "core/utils/networkUtilities.h" #include -#include -#include -#include #include #include #include @@ -50,36 +47,6 @@ bool Xray::startXray(const QString &cfg) amnezia_xray_setloghandler(ctxLogHandler, this); QByteArray bytes = cfg.toUtf8(); - - // Temporary aid for manual verification of SOCKS auth (inbounds.settings.accounts). - // Debug: always writes. Release: set env AMNEZIA_SAVE_XRAY_CONFIG=1 (if the daemon inherits it). - { - QString debugPath; - -#ifdef Q_OS_WIN - const QString dirPath = QStringLiteral("C:/Temp"); - QDir dir(dirPath); - - if (!dir.exists()) { - if (!dir.mkpath(".")) { - qWarning() << "[xray] failed to create directory:" << dirPath; - } - } - - debugPath = dir.filePath(QStringLiteral("amnezia-xray-config.json")); -#else - debugPath = QStringLiteral("/tmp/amnezia-xray-config.json"); -#endif - - QFile f(debugPath); - if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - f.write(bytes); - f.close(); - qDebug() << "[xray] wrote last config for testing:" << debugPath; - } else { - qWarning() << "[xray] failed to write test config to" << debugPath; - } - } if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { qDebug() << "[xray] configuration failed: " << err; amnezia_xray_free(err); From 4b1a0c078874822dbfb5d40cd9dfdbb074d84f6c Mon Sep 17 00:00:00 2001 From: dranik Date: Thu, 30 Apr 2026 16:53:17 +0300 Subject: [PATCH 14/18] fixed changes --- .../core/configurators/xrayConfigurator.cpp | 50 +++++++----- .../models/protocols/xrayProtocolConfig.cpp | 81 ++++++++++++------- .../models/protocols/xrayProtocolConfig.h | 16 ++-- .../ui/models/protocols/xrayConfigModel.cpp | 16 +++- .../qml/Pages2/PageProtocolXraySettings.qml | 24 +++--- 5 files changed, 113 insertions(+), 74 deletions(-) diff --git a/client/core/configurators/xrayConfigurator.cpp b/client/core/configurators/xrayConfigurator.cpp index e50110067..1d6e2240a 100644 --- a/client/core/configurators/xrayConfigurator.cpp +++ b/client/core/configurators/xrayConfigurator.cpp @@ -1,29 +1,28 @@ #include "xrayConfigurator.h" -#include "logger.h" #include -#include #include #include +#include #include +#include "logger.h" -#include "core/models/containerConfig.h" -#include "core/models/protocolConfig.h" -#include "core/models/protocols/xrayProtocolConfig.h" -#include "core/protocols/protocolUtils.h" -#include "core/utils/constants/configKeys.h" -#include "core/utils/constants/protocolConstants.h" #include "core/utils/containerEnum.h" #include "core/utils/containers/containerUtils.h" #include "core/utils/protocolEnum.h" -#include "core/utils/selfhosted/scriptsRegistry.h" #include "core/utils/selfhosted/sshSession.h" +#include "core/utils/selfhosted/scriptsRegistry.h" +#include "core/utils/protocolEnum.h" +#include "core/protocols/protocolUtils.h" +#include "core/utils/constants/configKeys.h" +#include "core/utils/constants/protocolConstants.h" +#include "core/models/containerConfig.h" +#include "core/models/protocols/xrayProtocolConfig.h" -namespace -{ +namespace { Logger logger("XrayConfigurator"); - QString normalizeXhttpMode(const QString &m) - { + + QString normalizeXhttpMode(const QString &m) { const QString t = m.trimmed(); if (t.isEmpty() || t.compare(QLatin1String("Auto"), Qt::CaseInsensitive) == 0) { return QStringLiteral("auto"); @@ -416,9 +415,9 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c } ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, - const ContainerConfig &containerConfig, - const DnsSettings &dnsSettings, - ErrorCode &errorCode) + const ContainerConfig &containerConfig, + const DnsSettings &dnsSettings, + ErrorCode &errorCode) { const XrayServerConfig *serverConfig = nullptr; if (const auto *xrayCfg = containerConfig.protocolConfig.as()) { @@ -428,7 +427,7 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia if (!serverConfig) { logger.error() << "No XrayProtocolConfig found"; errorCode = ErrorCode::InternalError; - return XrayProtocolConfig {}; + return XrayProtocolConfig{}; } const XrayServerConfig &srv = *serverConfig; @@ -436,7 +435,10 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode); if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { logger.error() << "Failed to prepare server config"; - return XrayProtocolConfig {}; + if (errorCode == ErrorCode::NoError) { + errorCode = ErrorCode::InternalError; + } + return XrayProtocolConfig{}; } // Fetch server keys (Reality only) @@ -448,7 +450,10 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia amnezia::protocols::xray::PublicKeyPath, errorCode); if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { logger.error() << "Failed to get public key"; - return XrayProtocolConfig {}; + if (errorCode == ErrorCode::NoError) { + errorCode = ErrorCode::InternalError; + } + return XrayProtocolConfig{}; } xrayPublicKey.replace("\n", ""); @@ -456,7 +461,10 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia amnezia::protocols::xray::shortidPath, errorCode); if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { logger.error() << "Failed to get short ID"; - return XrayProtocolConfig {}; + if (errorCode == ErrorCode::NoError) { + errorCode = ErrorCode::InternalError; + } + return XrayProtocolConfig{}; } xrayShortId.replace("\n", ""); } @@ -522,4 +530,4 @@ ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentia protocolConfig.setClientConfig(clientConfig); return protocolConfig; -} +} \ No newline at end of file diff --git a/client/core/models/protocols/xrayProtocolConfig.cpp b/client/core/models/protocols/xrayProtocolConfig.cpp index 194559e90..78ce75892 100644 --- a/client/core/models/protocols/xrayProtocolConfig.cpp +++ b/client/core/models/protocols/xrayProtocolConfig.cpp @@ -170,23 +170,46 @@ QJsonObject XrayServerConfig::toJson() const QJsonObject obj; // Existing fields - if (!port.isEmpty()) obj[configKey::port] = port; - if (!transportProto.isEmpty()) obj[configKey::transportProto] = transportProto; - if (!subnetAddress.isEmpty()) obj[configKey::subnetAddress] = subnetAddress; - if (!site.isEmpty()) obj[configKey::site] = site; - if (isThirdPartyConfig) obj[configKey::isThirdPartyConfig] = isThirdPartyConfig; + if (!port.isEmpty()) { + obj[configKey::port] = port; + } + if (!transportProto.isEmpty()) { + obj[configKey::transportProto] = transportProto; + } + if (!subnetAddress.isEmpty()) { + obj[configKey::subnetAddress] = subnetAddress; + } + if (!site.isEmpty()) { + obj[configKey::site] = site; + } + + if (isThirdPartyConfig) { + obj[configKey::isThirdPartyConfig] = isThirdPartyConfig; + } // New: Security - if (!security.isEmpty()) obj[configKey::xraySecurity] = security; - if (!flow.isEmpty()) obj[configKey::xrayFlow] = flow; - if (!fingerprint.isEmpty()) obj[configKey::xrayFingerprint] = fingerprint; - if (!sni.isEmpty()) obj[configKey::xraySni] = sni; - if (!alpn.isEmpty()) obj[configKey::xrayAlpn] = alpn; + if (!security.isEmpty()) { + obj[configKey::xraySecurity] = security; + } + if (!flow.isEmpty()) { + obj[configKey::xrayFlow] = flow; + } + if (!fingerprint.isEmpty()) { + obj[configKey::xrayFingerprint] = fingerprint; + } + if (!sni.isEmpty()) { + obj[configKey::xraySni] = sni; + } + if (!alpn.isEmpty()) { + obj[configKey::xrayAlpn] = alpn; + } // New: Transport - if (!transport.isEmpty()) obj[configKey::xrayTransport] = transport; + if (!transport.isEmpty()) { + obj[configKey::xrayTransport] = transport; + } obj["xhttp"] = xhttp.toJson(); - obj["mkcp"] = mkcp.toJson(); + obj["mkcp"] = mkcp.toJson(); return obj; } @@ -196,39 +219,39 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json) XrayServerConfig c; // Existing fields - c.port = json.value(configKey::port).toString(); - c.transportProto = json.value(configKey::transportProto).toString(); - c.subnetAddress = json.value(configKey::subnetAddress).toString(); - c.site = json.value(configKey::site).toString(); + c.port = json.value(configKey::port).toString(); + c.transportProto = json.value(configKey::transportProto).toString(); + c.subnetAddress = json.value(configKey::subnetAddress).toString(); + c.site = json.value(configKey::site).toString(); c.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false); // New: Security - c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity); - c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow); + c.security = json.value(configKey::xraySecurity).toString(protocols::xray::defaultSecurity); + c.flow = json.value(configKey::xrayFlow).toString(protocols::xray::defaultFlow); c.fingerprint = json.value(configKey::xrayFingerprint).toString(protocols::xray::defaultFingerprint); if (c.fingerprint.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) { c.fingerprint = QString::fromLatin1(protocols::xray::defaultFingerprint); } - c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni); - c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn); + c.sni = json.value(configKey::xraySni).toString(protocols::xray::defaultSni); + c.alpn = json.value(configKey::xrayAlpn).toString(protocols::xray::defaultAlpn); // New: Transport c.transport = json.value(configKey::xrayTransport).toString(protocols::xray::defaultTransport); - c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject()); - c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject()); + c.xhttp = XrayXhttpConfig::fromJson(json.value("xhttp").toObject()); + c.mkcp = XrayMkcpConfig::fromJson(json.value("mkcp").toObject()); return c; } bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const { - return port == other.port - && site == other.site - && security == other.security - && flow == other.flow - && transport == other.transport - && fingerprint == other.fingerprint - && sni == other.sni; + return port == other.port + && site == other.site + && security == other.security + && flow == other.flow + && transport == other.transport + && fingerprint == other.fingerprint + && sni == other.sni; } QJsonObject XrayClientConfig::toJson() const diff --git a/client/core/models/protocols/xrayProtocolConfig.h b/client/core/models/protocols/xrayProtocolConfig.h index 73c160bc7..eaf9abfd2 100644 --- a/client/core/models/protocols/xrayProtocolConfig.h +++ b/client/core/models/protocols/xrayProtocolConfig.h @@ -93,26 +93,26 @@ struct XrayMkcpConfig { // ── Server config (settings editable by user) ───────────────────────────────── struct XrayServerConfig { - // Existing fields QString port; QString transportProto; QString subnetAddress; QString site; - bool isThirdPartyConfig = false; + bool isThirdPartyConfig = false; // New: Security - QString security = protocols::xray::defaultSecurity; - QString flow = protocols::xray::defaultFlow; + QString security = protocols::xray::defaultSecurity; + QString flow = protocols::xray::defaultFlow; QString fingerprint = protocols::xray::defaultFingerprint; - QString sni = protocols::xray::defaultSni; - QString alpn = protocols::xray::defaultAlpn; + QString sni = protocols::xray::defaultSni; + QString alpn = protocols::xray::defaultAlpn; // New: Transport QString transport = protocols::xray::defaultTransport; XrayXhttpConfig xhttp; - XrayMkcpConfig mkcp; + XrayMkcpConfig mkcp; QJsonObject toJson() const; + static XrayServerConfig fromJson(const QJsonObject &json); bool hasEqualServerSettings(const XrayServerConfig &other) const; @@ -130,7 +130,7 @@ struct XrayClientConfig { // ── Top-level protocol config ────────────────────────────────────────────────── struct XrayProtocolConfig { - XrayServerConfig serverConfig; + XrayServerConfig serverConfig; std::optional clientConfig; QJsonObject toJson() const; diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index 6e215afcf..e81c7d76c 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -167,8 +167,7 @@ bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, i QVariant XrayConfigModel::data(const QModelIndex& index, int role) const { - if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) - { + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { return QVariant(); } @@ -252,14 +251,18 @@ QVariant XrayConfigModel::data(const QModelIndex& index, int role) const return QVariant(); } -void XrayConfigModel::updateModel(amnezia::DockerContainer container, - const amnezia::XrayProtocolConfig& protocolConfig) +void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig) { beginResetModel(); + m_container = container; + m_protocolConfig = protocolConfig; + applyDefaultsToServerConfig(m_protocolConfig.serverConfig); + m_originalProtocolConfig = m_protocolConfig; + endResetModel(); } @@ -269,6 +272,11 @@ void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &con config.port = protocols::xray::defaultPort; } + if (config.transportProto.isEmpty()) { + config.transportProto = ProtocolUtils::transportProtoToString( + ProtocolUtils::defaultTransportProto(amnezia::Proto::Xray), amnezia::Proto::Xray); + } + if (config.site.isEmpty()) { config.site = protocols::xray::defaultSite; } diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index b0188b7fb..39509fc04 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -46,7 +46,7 @@ PageType { delegate: ColumnLayout { width: listView.width - property alias focusItemId: portTextField.textField + property alias focusItemId: textFieldWithHeaderType.textField spacing: 0 @@ -87,7 +87,7 @@ PageType { } TextFieldWithHeaderType { - id: portTextField + id: textFieldWithHeaderType Layout.fillWidth: true Layout.topMargin: 32 Layout.leftMargin: 16 @@ -112,7 +112,7 @@ PageType { descriptionText: transport rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled - clickedFunction: function () { + clickedFunction: function() { PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings) } } @@ -126,7 +126,7 @@ PageType { descriptionText: security rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled - clickedFunction: function () { + clickedFunction: function() { PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings) } } @@ -140,7 +140,7 @@ PageType { descriptionText: flow rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled - clickedFunction: function () { + clickedFunction: function() { PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings) } } @@ -158,15 +158,15 @@ PageType { Layout.bottomMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - enabled: portTextField.errorText === "" + enabled: textFieldWithHeaderType.errorText === "" text: qsTr("Save") - onClicked: function () { + onClicked: function() { forceActiveFocus() var headerText = qsTr("Save settings?") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var yesButtonText = qsTr("Continue") var noButtonText = qsTr("Cancel") - var yesButtonFunction = function () { + var yesButtonFunction = function() { if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) { PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection")) return @@ -174,7 +174,7 @@ PageType { PageController.goToPage(PageEnum.PageSetupWizardInstalling) InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray) } - var noButtonFunction = function () { + var noButtonFunction = function() { if (!GC.isMobile()) saveButton.forceActiveFocus() } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -188,12 +188,12 @@ PageType { text: qsTr("Reset settings") textColor: AmneziaStyle.color.vibrantRed visible: listView.enabled - clickedFunction: function () { - var yesButtonFunction = function () { + clickedFunction: function() { + var yesButtonFunction = function() { XrayConfigModel.resetToDefaults() } showQuestionDrawer(qsTr("Reset settings?"), qsTr("All XRay settings will be restored to defaults."), - qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function () { + qsTr("Reset"), qsTr("Cancel"), yesButtonFunction, function() { }) } } From ef82b589c85e1923a164390e067771ad38b4d6e3 Mon Sep 17 00:00:00 2001 From: dranik Date: Mon, 4 May 2026 14:08:47 +0300 Subject: [PATCH 15/18] fixed conf --- client/core/models/protocols/xrayProtocolConfig.cpp | 11 ++++++++--- client/ui/models/protocols/xrayConfigModel.cpp | 6 ++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/client/core/models/protocols/xrayProtocolConfig.cpp b/client/core/models/protocols/xrayProtocolConfig.cpp index 78ce75892..a6c0043bc 100644 --- a/client/core/models/protocols/xrayProtocolConfig.cpp +++ b/client/core/models/protocols/xrayProtocolConfig.cpp @@ -287,7 +287,10 @@ XrayClientConfig XrayClientConfig::fromJson(const QJsonObject &json) if (vnextObj.contains(protocols::xray::users)) { QJsonArray users = vnextObj[protocols::xray::users].toArray(); if (!users.isEmpty()) { - c.id = users[0].toObject().value(protocols::xray::id).toString(); + QJsonObject user = users[0].toObject(); + if (user.contains(protocols::xray::id)) { + c.id = user[protocols::xray::id].toString(); + } } } } @@ -335,8 +338,10 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json) if (parsed.contains(protocols::xray::inbounds)) { QJsonArray inbounds = parsed.value(protocols::xray::inbounds).toArray(); if (!inbounds.isEmpty()) { - clientCfg.localPort = QString::number( - inbounds[0].toObject().value(protocols::xray::port).toInt()); + QJsonObject inbound = inbounds[0].toObject(); + if (inbound.contains(protocols::xray::port)) { + clientCfg.localPort = QString::number(inbound.value(protocols::xray::port).toInt()); + } } } c.clientConfig = clientCfg; diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index e81c7d76c..9b7409e3e 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -344,8 +344,10 @@ void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &con amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig() { - if (!m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig)) - { + const bool serverSettingsChanged = + !m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig); + + if (serverSettingsChanged) { m_protocolConfig.clearClientConfig(); } return m_protocolConfig; From ad21dc03e3edddfe8da41d8af6c790ed91d7af9e Mon Sep 17 00:00:00 2001 From: dranik Date: Fri, 8 May 2026 12:31:30 +0300 Subject: [PATCH 16/18] fixed UI --- client/translations/amneziavpn_ru_RU.ts | 15 +++++++++ .../ui/models/protocols/xrayConfigModel.cpp | 33 +++++++++++++++++-- client/ui/models/protocols/xrayConfigModel.h | 7 +++- client/ui/qml/Controls2/MinMaxRowType.qml | 2 +- .../Pages2/PageProtocolXrayFlowSettings.qml | 8 +++-- .../PageProtocolXraySecuritySettings.qml | 8 +++-- .../qml/Pages2/PageProtocolXraySettings.qml | 30 ++++++++++++++--- .../PageProtocolXrayTransportSettings.qml | 26 ++++++--------- .../PageProtocolXrayXPaddingBytesSettings.qml | 8 +++-- .../PageProtocolXrayXPaddingSettings.qml | 8 +++-- .../Pages2/PageProtocolXrayXmuxSettings.qml | 8 +++-- client/ui/qml/Pages2/PageStart.qml | 5 +-- 12 files changed, 121 insertions(+), 37 deletions(-) diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index e813b40f1..b1f95a688 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -1312,6 +1312,21 @@ Thank you for staying with us! PageProtocolXraySettings + + + XRay VLESS settings + Настройки XRay VLESS + + + + More about settings + Подробнее о настройках + + + + Reset settings + Сбросить настройки + XRay settings diff --git a/client/ui/models/protocols/xrayConfigModel.cpp b/client/ui/models/protocols/xrayConfigModel.cpp index 9b7409e3e..9a24759c9 100644 --- a/client/ui/models/protocols/xrayConfigModel.cpp +++ b/client/ui/models/protocols/xrayConfigModel.cpp @@ -20,11 +20,15 @@ int XrayConfigModel::rowCount(const QModelIndex& parent) const bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() < 0 || index.row() >= ContainerUtils::allContainers().size()) + // This model always has a single row (row 0). Using rowCount() avoids + // coupling editing ability to global container list size. + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { return false; } + const bool wasUnsavedChanges = hasUnsavedChanges(); + auto& srv = m_protocolConfig.serverConfig; auto& xhttp = srv.xhttp; auto& mkcp = srv.mkcp; @@ -162,6 +166,9 @@ bool XrayConfigModel::setData(const QModelIndex& index, const QVariant& value, i } emit dataChanged(index, index, QList{role}); + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } return true; } @@ -253,6 +260,8 @@ QVariant XrayConfigModel::data(const QModelIndex& index, int role) const void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig) { + const bool wasUnsavedChanges = hasUnsavedChanges(); + beginResetModel(); m_container = container; @@ -264,6 +273,9 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne m_originalProtocolConfig = m_protocolConfig; endResetModel(); + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } } void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config) @@ -353,11 +365,16 @@ amnezia::XrayProtocolConfig XrayConfigModel::getProtocolConfig() return m_protocolConfig; } -bool XrayConfigModel::isServerSettingsEqual() +bool XrayConfigModel::isServerSettingsEqual() const { return m_protocolConfig.serverConfig.hasEqualServerSettings(m_originalProtocolConfig.serverConfig); } +bool XrayConfigModel::hasUnsavedChanges() const +{ + return !isServerSettingsEqual(); +} + QHash XrayConfigModel::roleNames() const { QHash roles; @@ -435,20 +452,32 @@ QHash XrayConfigModel::roleNames() const void XrayConfigModel::resetToDefaults() { + const bool wasUnsavedChanges = hasUnsavedChanges(); + beginResetModel(); m_protocolConfig.serverConfig = amnezia::XrayServerConfig{}; applyDefaultsToServerConfig(m_protocolConfig.serverConfig); endResetModel(); + + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } } void XrayConfigModel::applyServerConfig(const amnezia::XrayServerConfig &serverConfig) { + const bool wasUnsavedChanges = hasUnsavedChanges(); + beginResetModel(); m_protocolConfig.serverConfig = serverConfig; // Clear client config since server settings changed m_protocolConfig.clearClientConfig(); m_originalProtocolConfig = m_protocolConfig; endResetModel(); + + if (wasUnsavedChanges != hasUnsavedChanges()) { + emit hasUnsavedChangesChanged(); + } } QStringList XrayConfigModel::flowOptions() diff --git a/client/ui/models/protocols/xrayConfigModel.h b/client/ui/models/protocols/xrayConfigModel.h index c317e955a..fee066107 100644 --- a/client/ui/models/protocols/xrayConfigModel.h +++ b/client/ui/models/protocols/xrayConfigModel.h @@ -12,6 +12,7 @@ class XrayConfigModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged) public: enum Roles @@ -120,10 +121,14 @@ public: public slots: void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig); amnezia::XrayProtocolConfig getProtocolConfig(); - bool isServerSettingsEqual(); + bool isServerSettingsEqual() const; + bool hasUnsavedChanges() const; void resetToDefaults(); void applyServerConfig(const amnezia::XrayServerConfig &serverConfig); +signals: + void hasUnsavedChangesChanged(); + protected: QHash roleNames() const override; diff --git a/client/ui/qml/Controls2/MinMaxRowType.qml b/client/ui/qml/Controls2/MinMaxRowType.qml index 130de293a..90a7fae7b 100644 --- a/client/ui/qml/Controls2/MinMaxRowType.qml +++ b/client/ui/qml/Controls2/MinMaxRowType.qml @@ -30,7 +30,7 @@ Item { RowLayout { id: row anchors.fill: parent - spacing: 8 + spacing: 10 // Min field TextFieldWithHeaderType { diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml index 53ff9be53..e5e7e1183 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml @@ -26,7 +26,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -44,6 +44,9 @@ PageType { Layout.topMargin: 0 Layout.bottomMargin: 24 text: qsTr("Flow") + font.pixelSize: 36 + lineHeight: 38 + LanguageUiController.getLineHeightAppend() + font.letterSpacing: -1.08 } VerticalRadioButton { @@ -98,7 +101,8 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - enabled: listView.enabled + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible text: qsTr("Save") clickedFunc: function () { var headerText = qsTr("Save settings?") diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml index 0ef419d50..7dd5c9229 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -26,7 +26,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -44,6 +44,9 @@ PageType { Layout.topMargin: 0 Layout.bottomMargin: 24 text: qsTr("Security") + font.pixelSize: 36 + lineHeight: 38 + LanguageUiController.getLineHeightAppend() + font.letterSpacing: -1.08 } VerticalRadioButton { @@ -265,7 +268,8 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - enabled: listView.enabled + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible text: qsTr("Save") clickedFunc: function () { var headerText = qsTr("Save settings?") diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 39509fc04..b9f968339 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -17,6 +17,20 @@ import "../Components" PageType { id: root + function formatTransport(value) { + if (value === "raw") return "RAW (TCP)" + if (value === "xhttp") return "XHTTP" + if (value === "mkcp") return "mKCP" + return value + } + + function formatSecurity(value) { + if (value === "none") return "None" + if (value === "tls") return "TLS" + if (value === "reality") return "Reality" + return value + } + BackButtonType { id: backButton @@ -60,6 +74,9 @@ PageType { Layout.fillWidth: true text: qsTr("XRay VLESS settings") wrapMode: Text.WordWrap + font.pixelSize: 36 + lineHeight: 38 + LanguageUiController.getLineHeightAppend() + font.letterSpacing: -1.08 } ImageButtonType { @@ -67,7 +84,7 @@ PageType { implicitWidth: 40 implicitHeight: 40 image: "qrc:/images/controls/more-vertical.svg" - imageColor: AmneziaStyle.color.mutedGray + imageColor: AmneziaStyle.color.paleGray onClicked: PageController.goToPage(PageEnum.PageProtocolXraySnapshots) } } @@ -78,7 +95,9 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 4 text: qsTr("More about settings") - color: AmneziaStyle.color.burntOrange + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 16 + lineHeight: 24 + LanguageUiController.getLineHeightAppend() MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor @@ -109,7 +128,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 16 text: qsTr("Transport") - descriptionText: transport + descriptionText: root.formatTransport(transport) rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled clickedFunction: function() { @@ -123,7 +142,7 @@ PageType { LabelWithButtonType { Layout.fillWidth: true text: qsTr("Security") - descriptionText: security + descriptionText: root.formatSecurity(security) rightImageSource: "qrc:/images/controls/chevron-right.svg" enabled: listView.enabled clickedFunction: function() { @@ -158,7 +177,8 @@ PageType { Layout.bottomMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - enabled: textFieldWithHeaderType.errorText === "" + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible && textFieldWithHeaderType.errorText === "" text: qsTr("Save") onClicked: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml index c62cd82bc..06e519385 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -26,7 +26,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -44,6 +44,9 @@ PageType { Layout.topMargin: 0 Layout.bottomMargin: 24 text: qsTr("Transport") + font.pixelSize: 36 + lineHeight: 38 + LanguageUiController.getLineHeightAppend() + font.letterSpacing: -1.08 } // ── Radio buttons ───────────────────────────────────────── @@ -53,7 +56,7 @@ PageType { Layout.rightMargin: 16 text: qsTr("RAW (TCP)") checked: transport === "raw" - onClicked: transport = "raw" + onToggled: if (checked && transport !== "raw") transport = "raw" } DividerType { @@ -66,7 +69,7 @@ PageType { text: qsTr("XHTTP") descriptionText: qsTr("Advanced users") checked: transport === "xhttp" - onClicked: transport = "xhttp" + onToggled: if (checked && transport !== "xhttp") transport = "xhttp" } DividerType { @@ -78,7 +81,7 @@ PageType { Layout.rightMargin: 16 text: qsTr("mKCP") checked: transport === "mkcp" - onClicked: transport = "mkcp" + onToggled: if (checked && transport !== "mkcp") transport = "mkcp" } DividerType { @@ -185,20 +188,10 @@ PageType { Layout.fillWidth: true spacing: 0 - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("Transport Mode") - color: AmneziaStyle.color.mutedGray - } - DropDownType { id: modeDropDown Layout.fillWidth: true - Layout.topMargin: 0 + Layout.topMargin: 16 Layout.leftMargin: 16 Layout.rightMargin: 16 text: xhttpMode @@ -738,7 +731,8 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - enabled: listView.enabled + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible text: qsTr("Save") clickedFunc: function () { var headerText = qsTr("Save settings?") diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml index 08116eaf0..bfd6fef46 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml @@ -26,7 +26,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -44,6 +44,9 @@ PageType { Layout.topMargin: 0 Layout.bottomMargin: 24 text: qsTr("xPaddingBytes") + font.pixelSize: 36 + lineHeight: 38 + LanguageUiController.getLineHeightAppend() + font.letterSpacing: -1.08 } CaptionTextType { @@ -81,7 +84,8 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - enabled: listView.enabled + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible text: qsTr("Save") clickedFunc: function () { var headerText = qsTr("Save settings?") diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml index 9176eaf48..3f9cfcd67 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -26,7 +26,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -44,6 +44,9 @@ PageType { Layout.topMargin: 0 Layout.bottomMargin: 24 text: qsTr("xPadding") + font.pixelSize: 36 + lineHeight: 38 + LanguageUiController.getLineHeightAppend() + font.letterSpacing: -1.08 } // xPaddingBytes — min/max display row @@ -197,7 +200,8 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - enabled: listView.enabled + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible text: qsTr("Save") clickedFunc: function () { var headerText = qsTr("Save settings?") diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml index 24793c4e0..6c52843e8 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -26,7 +26,7 @@ PageType { ListViewType { id: listView anchors.top: backButton.bottom - anchors.bottom: saveButton.top + anchors.bottom: saveButton.visible ? saveButton.top : parent.bottom anchors.left: parent.left anchors.right: parent.right @@ -44,6 +44,9 @@ PageType { Layout.topMargin: 0 Layout.bottomMargin: 24 text: qsTr("xmux") + font.pixelSize: 36 + lineHeight: 38 + LanguageUiController.getLineHeightAppend() + font.letterSpacing: -1.08 } SwitcherType { @@ -194,7 +197,8 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - enabled: listView.enabled + visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + enabled: visible text: qsTr("Save") clickedFunc: function () { var headerText = qsTr("Save settings?") diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index d39baa9f8..65ca391e3 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -312,8 +312,9 @@ PageType { topPadding: 8 bottomPadding: 8 + PageController.safeAreaBottomMargin - leftPadding: 96 - rightPadding: 96 + leftPadding: 24 + rightPadding: 24 + spacing: 24 height: visible ? homeTabButton.implicitHeight + tabBar.topPadding + tabBar.bottomPadding : 0 From 6e6b3f24aab8a6c9b2194e6ced6644431698a307 Mon Sep 17 00:00:00 2001 From: dranik Date: Fri, 8 May 2026 13:48:49 +0300 Subject: [PATCH 17/18] fixed size & button save --- client/ui/qml/Pages2/PageProtocolXraySettings.qml | 3 ++- client/ui/qml/Pages2/PageStart.qml | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index b9f968339..ed18e35ad 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -177,7 +177,8 @@ PageType { Layout.bottomMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 - visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + // Show Save immediately while user edits port, even before focus loss. + visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || textFieldWithHeaderType.textField.text !== port) enabled: visible && textFieldWithHeaderType.errorText === "" text: qsTr("Save") onClicked: function() { diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 65ca391e3..d39baa9f8 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -312,9 +312,8 @@ PageType { topPadding: 8 bottomPadding: 8 + PageController.safeAreaBottomMargin - leftPadding: 24 - rightPadding: 24 - spacing: 24 + leftPadding: 96 + rightPadding: 96 height: visible ? homeTabButton.implicitHeight + tabBar.topPadding + tabBar.bottomPadding : 0 From 9ac2184ccebd6467545014f9fa9896d67ac03f8e Mon Sep 17 00:00:00 2001 From: dranik Date: Wed, 13 May 2026 14:13:39 +0300 Subject: [PATCH 18/18] fixed build iOS --- client/platforms/ios/ios_controller.mm | 2 +- client/ui/controllers/api/subscriptionUiController.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 73aa02484..150ae7261 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -220,7 +220,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur m_rawConfig = configuration; m_serverAddress = configuration.value(configKey::hostName).toString().toNSString(); - const QString serverDescription = configuration.value(config_key::description).toString().trimmed(); + const QString serverDescription = configuration.value(configKey::description).toString().trimmed(); QString tunnelName; if (serverDescription.isEmpty()) { tunnelName = ProtocolUtils::protoToString(proto); diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index 75d96553e..1c7c7656b 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -9,6 +9,11 @@ #include "core/utils/qrCodeUtils.h" #include "ui/controllers/systemController.h" #include "version.h" + +#ifdef Q_OS_IOS +#include "platforms/ios/ios_controller.h" +#include +#endif #include "core/models/serverConfig.h" #include #include