From 047e4be2077e9b99007f34e53f43fddc49ffbe51 Mon Sep 17 00:00:00 2001 From: penguinehis Date: Mon, 13 Jul 2026 01:34:28 -0300 Subject: [PATCH] XHTTP Panel Fix --- README.md | 4 + admin/assets/app.css | 4 +- admin/assets/js/01-core.js | 6 +- admin/assets/js/09-xray-wizard.js | 120 +++++++++++++++++++++++++++++- admin/index.html | 34 +++++---- xray_native_test.go | 2 +- 6 files changed, 151 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 0d048e6..f90bd66 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ Na aba **Xray → Configuração Xray → Visual**, o cartão **Shared XHTTP end O listener HTTP interno escolhe o caminho mais específico, portanto `/ssh` é encaminhado ao servidor SSH antes do inbound raiz. Todos os inbounds na mesma porta devem usar a mesma opção TLS e o mesmo certificado. Esse recurso exige o modo **Internal native emulator**; o binário Xray externo não reconhece o protocolo `ssh` específico do DragonCore. +Para configurações XHTTP antigas, carregue a configuração visual e clique em **Ativar SSH /ssh** no cartão VLESS/VMess existente. O painel preserva o inbound original, seus clientes, path, host, modo, opções avançadas e TLS; cria apenas o inbound SSH correspondente na mesma porta e salva/reinicia o Xray. Não é necessário recriar clientes ou o endpoint. + ### Requisitos - Servidor Linux com `systemd` @@ -581,6 +583,8 @@ In **Xray → Xray Config → Visual**, the **Shared XHTTP endpoint** card creat The internal HTTP listener selects the most specific path, so `/ssh` reaches the SSH server before the root inbound. Every inbound sharing a port must use the same TLS setting and certificate. This feature requires **Internal native emulator** mode; the external Xray binary does not recognize DragonCore's custom `ssh` protocol. +For older XHTTP configurations, load the visual configuration and click **Enable SSH /ssh** on the existing VLESS/VMess card. The panel preserves the original inbound, clients, path, host, mode, advanced options, and TLS; it only creates the matching SSH inbound on the same port and then saves/restarts Xray. There is no need to recreate clients or the endpoint. + ### Requirements - Linux server with `systemd` diff --git a/admin/assets/app.css b/admin/assets/app.css index 01d3d58..bd18313 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -674,12 +674,14 @@ select:disabled { .shared-endpoint-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.shared-endpoint-head h3,.visual-editor-heading h3{margin-top:5px;font-size:1.12rem;letter-spacing:-.02em}.shared-endpoint-head p{margin-top:5px;color:var(--muted);font-size:.75rem;line-height:1.45}.shared-endpoint-head code,.shared-route-preview code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#c8bbff} .shared-route-preview{display:grid;grid-template-columns:1fr 48px 1fr;align-items:center;gap:8px;margin:17px 0;padding:10px;border:1px solid rgba(148,163,184,.11);border-radius:17px;background:rgba(255,255,255,.025)}.shared-route-preview span{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 12px;border:1px solid rgba(139,92,246,.16);border-radius:13px;background:rgba(139,92,246,.07)}.shared-route-preview strong{font-size:.78rem}.shared-route-preview code{font-size:.77rem;font-weight:900}.shared-route-preview i{height:1px;background:linear-gradient(90deg,rgba(139,92,246,.2),rgba(34,211,238,.7),rgba(139,92,246,.2));position:relative}.shared-route-preview i::after{content:"";position:absolute;right:0;top:-3px;width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px var(--accent)} .shared-endpoint-grid{grid-template-columns:repeat(3,minmax(0,1fr));}.shared-endpoint-actions{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:15px;padding-top:14px;border-top:1px solid rgba(148,163,184,.1)}.shared-endpoint-actions .hint{max-width:650px} +.legacy-xhttp-migration{display:flex;align-items:center;gap:13px;margin:-3px 0 15px;padding:13px 15px;border:1px solid rgba(49,214,123,.19);border-radius:18px;background:linear-gradient(135deg,rgba(49,214,123,.075),rgba(34,211,238,.035));color:var(--text-2)}.legacy-xhttp-icon{display:grid;place-items:center;flex:0 0 auto;width:42px;height:42px;border:1px solid rgba(49,214,123,.25);border-radius:14px;background:rgba(49,214,123,.11);color:#72e6a4;font-size:.68rem;font-weight:950;letter-spacing:.035em}.legacy-xhttp-migration strong{display:block;color:var(--text);font-size:.8rem}.legacy-xhttp-migration p{margin-top:3px;color:var(--muted);font-size:.71rem;line-height:1.5} .visual-config-toolbar{display:grid;grid-template-columns:180px minmax(0,1fr) auto;align-items:end;gap:14px;margin-bottom:13px;padding:13px 15px;border:1px solid rgba(148,163,184,.1);border-radius:18px;background:rgba(255,255,255,.025)}.visual-config-toolbar-copy{display:flex;flex-direction:column;gap:4px;padding-bottom:4px}.visual-config-toolbar-copy strong{font-size:.84rem}.visual-config-toolbar-copy span{color:var(--muted);font-size:.71rem} .visual-inbound-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:14px}.visual-inbound-card{position:relative;display:flex;flex-direction:column;gap:13px;min-width:0;padding:15px;border:1px solid rgba(148,163,184,.11);border-radius:18px;background:rgba(255,255,255,.027);transition:.15s ease}.visual-inbound-card:hover{border-color:rgba(139,92,246,.3);background:rgba(139,92,246,.045);transform:translateY(-1px)}.visual-inbound-card-head,.visual-inbound-meta,.visual-inbound-actions{display:flex;align-items:center;gap:8px}.visual-inbound-card-head{justify-content:space-between}.visual-inbound-name{min-width:0}.visual-inbound-name strong{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--text);font-size:.84rem;white-space:nowrap}.visual-inbound-name small{display:block;margin-top:4px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.67rem}.visual-inbound-meta{flex-wrap:wrap}.visual-inbound-meta span{padding:4px 7px;border-radius:8px;background:rgba(148,163,184,.07);color:var(--muted);font-size:.67rem}.visual-inbound-actions{justify-content:flex-end;margin-top:auto;padding-top:11px;border-top:1px solid rgba(148,163,184,.08)} +.legacy-ssh-btn{margin-right:auto;border-color:rgba(49,214,123,.3)!important;background:linear-gradient(135deg,rgba(49,214,123,.18),rgba(34,211,238,.09))!important;color:#8af0b5!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}.legacy-ssh-btn:hover:not(:disabled){border-color:rgba(49,214,123,.52)!important;transform:translateY(-1px)}.legacy-ssh-btn.is-enabled:disabled{opacity:1;border-color:rgba(49,214,123,.16)!important;background:rgba(49,214,123,.07)!important;color:#72b98e!important;cursor:default} .visual-inbound-editor{margin:14px 0;padding:18px;border:1px solid rgba(34,211,238,.2);border-radius:22px;background:radial-gradient(circle at 100% 0,rgba(34,211,238,.09),transparent 28%),rgba(6,10,16,.86)}.visual-editor-heading{--hero-accent:34,211,238;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:15px;padding-bottom:13px;border-bottom:1px solid rgba(148,163,184,.1)}.visual-save-bar{position:sticky;bottom:14px;z-index:8;display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:16px;padding:13px 15px;border:1px solid rgba(139,92,246,.2);border-radius:19px;background:rgba(8,12,20,.9);box-shadow:0 18px 48px rgba(0,0,0,.35);backdrop-filter:blur(16px)} @media(max-width:1100px){.shared-endpoint-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.visual-inbound-list{grid-template-columns:1fr}} -@media(max-width:760px){.page-hero{min-height:0;padding:20px;border-radius:22px;align-items:flex-start;flex-direction:column}.page-hero h2{font-size:1.55rem}.page-hero-pills{max-width:none;justify-content:flex-start}.page-hero-mark{width:50px;height:50px;border-radius:17px}.shared-endpoint-card{padding:15px}.shared-endpoint-head,.shared-endpoint-actions,.visual-save-bar{align-items:flex-start;flex-direction:column}.shared-endpoint-actions .btn,.visual-save-bar .btn{width:100%}.shared-route-preview{grid-template-columns:1fr}.shared-route-preview i{width:1px;height:22px;justify-self:center}.shared-route-preview i::after{right:-3px;top:auto;bottom:0}.shared-endpoint-grid{grid-template-columns:1fr!important}.visual-config-toolbar{grid-template-columns:1fr;align-items:stretch}.visual-config-toolbar .btn{width:100%}} +@media(max-width:760px){.page-hero{min-height:0;padding:20px;border-radius:22px;align-items:flex-start;flex-direction:column}.page-hero h2{font-size:1.55rem}.page-hero-pills{max-width:none;justify-content:flex-start}.page-hero-mark{width:50px;height:50px;border-radius:17px}.shared-endpoint-card{padding:15px}.shared-endpoint-head,.shared-endpoint-actions,.visual-save-bar{align-items:flex-start;flex-direction:column}.shared-endpoint-actions .btn,.visual-save-bar .btn{width:100%}.shared-route-preview{grid-template-columns:1fr}.shared-route-preview i{width:1px;height:22px;justify-self:center}.shared-route-preview i::after{right:-3px;top:auto;bottom:0}.shared-endpoint-grid{grid-template-columns:1fr!important}.legacy-xhttp-migration{align-items:flex-start}.visual-config-toolbar{grid-template-columns:1fr;align-items:stretch}.visual-config-toolbar .btn{width:100%}.visual-inbound-actions{align-items:stretch;flex-wrap:wrap}.legacy-ssh-btn{flex:1 0 100%;margin-right:0}} /* --- Bot sales workspace --- */ #tab-bot{--bot-accent:#7c5cff;--bot-line:rgba(160,174,192,.14);} diff --git a/admin/assets/js/01-core.js b/admin/assets/js/01-core.js index d406eb1..605efb3 100644 --- a/admin/assets/js/01-core.js +++ b/admin/assets/js/01-core.js @@ -121,7 +121,8 @@ Object.assign(I18N_TEXT["en-US"], { "Fleet control":"Fleet control","Fleet control copy":"Add nodes, test credentials, and configure remote infrastructure.","Live fleet":"Live fleet","Server status copy":"Health, load, and active sessions for every managed node.","Observability":"Observability","Monitoring copy":"Server resources, interfaces, and capacity in real time.", "Traffic intelligence":"Traffic intelligence","Network traffic":"Network traffic","Network traffic copy":"Daily and monthly history for understanding infrastructure usage.","Diagnostics":"Diagnostics","System logs copy":"Investigate the panel, DNSTT, and Xray in a focused view.","System studio":"System studio","Settings copy":"Network, SSH, tunnels, and TLS organized visually and applied live.", "One domain and port":"One domain and port","Shared endpoint copy":"The selected protocol uses /; SSH uses /ssh. Available in native Xray mode.","Protocol on /":"Protocol on /","Shared port":"Shared port","Listen IP":"Listen IP","HTTP host":"HTTP host","optional":"optional","XHTTP mode":"XHTTP mode","Security":"Security","No TLS":"No TLS","Certificate file":"Certificate file","Key file":"Key file","Create / update endpoint":"Create / update endpoint", - "Configured inbounds":"Configured inbounds","Visual inbound help":"Edit any card visually or use JSON for advanced fields.","New inbound":"New inbound","Visual editor":"Visual editor","Add inbound":"Add inbound","Save changes":"Save changes","Duplicate":"Duplicate","Remove":"Remove","Save config and restart":"Save config and restart" + "Configured inbounds":"Configured inbounds","Visual inbound help":"Edit any card visually or use JSON for advanced fields.","New inbound":"New inbound","Visual editor":"Visual editor","Add inbound":"Add inbound","Save changes":"Save changes","Duplicate":"Duplicate","Remove":"Remove","Save config and restart":"Save config and restart", + "Old XHTTP configuration?":"Old XHTTP configuration?","Legacy XHTTP migration copy":"Use “Enable SSH /ssh” on the existing inbound card. The panel preserves the inbound, clients, and every current option; it only adds the SSH route and restarts Xray.","Enable SSH /ssh":"Enable SSH /ssh","SSH /ssh enabled":"SSH /ssh enabled","Legacy XHTTP migration":"Legacy XHTTP migration" }); Object.assign(I18N_TEXT["pt-BR"], { "Command center":"Central de comando","Overview command copy":"Contas, conexões e infraestrutura em uma leitura rápida.","Access workspace":"Área de acessos","SSH accounts":"Contas SSH","SSH accounts copy":"Crie, limite e acompanhe acessos SSH e SlowDNS com segurança.", @@ -129,7 +130,8 @@ Object.assign(I18N_TEXT["pt-BR"], { "Fleet control":"Controle da frota","Fleet control copy":"Adicione nós, teste credenciais e configure a infraestrutura remota.","Live fleet":"Frota ao vivo","Server status copy":"Saúde, carga e sessões ativas de cada nó gerenciado.","Observability":"Observabilidade","Monitoring copy":"Recursos, interfaces e capacidade do servidor em tempo real.", "Traffic intelligence":"Inteligência de tráfego","Network traffic":"Tráfego de rede","Network traffic copy":"Histórico diário e mensal para entender o consumo da infraestrutura.","Diagnostics":"Diagnóstico","System logs copy":"Investigue painel, DNSTT e Xray com uma visualização focada.","System studio":"Estúdio do sistema","Settings copy":"Rede, SSH, túneis e TLS organizados em blocos visuais e aplicados ao vivo.", "One domain and port":"Um domínio e uma porta","Shared endpoint copy":"O protocolo selecionado usa /; SSH usa /ssh. Disponível no modo Xray nativo.","Protocol on /":"Protocolo em /","Shared port":"Porta compartilhada","Listen IP":"IP de listen","HTTP host":"Host HTTP","optional":"opcional","XHTTP mode":"Modo XHTTP","Security":"Segurança","No TLS":"Sem TLS","Certificate file":"Arquivo do certificado","Key file":"Arquivo da chave","Create / update endpoint":"Criar / atualizar endpoint", - "Configured inbounds":"Inbounds configurados","Visual inbound help":"Edite qualquer cartão visualmente ou use JSON para campos avançados.","New inbound":"Novo inbound","Visual editor":"Editor visual","Add inbound":"Adicionar inbound","Save changes":"Salvar alterações","Duplicate":"Duplicar","Remove":"Remover","Save config and restart":"Salvar configuração e reiniciar" + "Configured inbounds":"Inbounds configurados","Visual inbound help":"Edite qualquer cartão visualmente ou use JSON para campos avançados.","New inbound":"Novo inbound","Visual editor":"Editor visual","Add inbound":"Adicionar inbound","Save changes":"Salvar alterações","Duplicate":"Duplicar","Remove":"Remover","Save config and restart":"Salvar configuração e reiniciar", + "Old XHTTP configuration?":"Configuração XHTTP antiga?","Legacy XHTTP migration copy":"Use “Ativar SSH /ssh” no cartão do inbound existente. O painel mantém o inbound, os clientes e todas as opções atuais; adiciona somente a rota SSH e reinicia o Xray.","Enable SSH /ssh":"Ativar SSH /ssh","SSH /ssh enabled":"SSH /ssh ativado","Legacy XHTTP migration":"Migração de configuração XHTTP antiga" }); const I18N_REVERSE = Object.fromEntries(SUPPORTED_LANGS.map(lang => [lang, Object.fromEntries(Object.entries(I18N_TEXT[lang] || {}).map(([k, v]) => [v, k]))])); let currentLang = detectInitialLanguage(); diff --git a/admin/assets/js/09-xray-wizard.js b/admin/assets/js/09-xray-wizard.js index 1ee0f6c..b09e89a 100644 --- a/admin/assets/js/09-xray-wizard.js +++ b/admin/assets/js/09-xray-wizard.js @@ -152,6 +152,18 @@ function renderWzInbounds() { const actions = document.createElement("div"); actions.className = "visual-inbound-actions"; + const xhttp = visualXHTTPSettings(ib); + const proxyProtocol = String(ib.protocol || "").toLowerCase(); + if (xhttp && ["vless", "vmess"].includes(proxyProtocol)) { + const sshRoute = findSSHRouteForInbound(ib); + const sshBtn = document.createElement("button"); + sshBtn.className = sshRoute ? "btn btn-soft btn-sm legacy-ssh-btn is-enabled" : "btn btn-sm legacy-ssh-btn"; + sshBtn.type = "button"; + sshBtn.textContent = t(sshRoute ? "SSH /ssh enabled" : "Enable SSH /ssh"); + sshBtn.disabled = !!sshRoute; + sshBtn.onclick = () => enableSSHForWzInbound(i); + actions.appendChild(sshBtn); + } const duplicateBtn = document.createElement("button"); duplicateBtn.className = "btn btn-ghost btn-sm"; duplicateBtn.type = "button"; @@ -310,6 +322,108 @@ function visualXHTTPSettings(ib) { return ss.xhttpSettings || ss.splithttpSettings || {}; } +function sameVisualEndpoint(a, b) { + return String(a?.listen || "0.0.0.0") === String(b?.listen || "0.0.0.0") && String(a?.port) === String(b?.port); +} + +function findSSHRouteForInbound(source) { + return wzInbounds.find(candidate => { + const xh = visualXHTTPSettings(candidate); + return sameVisualEndpoint(source, candidate) && !!xh && String(candidate?.protocol || "").toLowerCase() === "ssh" && normalizeVisualPath(xh.path) === "/ssh"; + }) || null; +} + +function uniqueLegacySSHTag(source) { + const tags = new Set(wzInbounds.map(ib => String(ib?.tag || ""))); + const sourceTag = String(source?.tag || source?.port || "xhttp").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "xhttp"; + let tag = `ssh-${sourceTag}`; + let suffix = 2; + while (tags.has(tag)) tag = `ssh-${sourceTag}-${suffix++}`; + return tag; +} + +// Adds SSH to a legacy VLESS/VMess XHTTP listener without rebuilding or +// modifying the original inbound. The companion inherits its listen address, +// port, host, XHTTP mode, advanced transport options, TLS certificate, and key; +// only its protocol, empty SSH settings, tag, and /ssh path differ. +async function enableSSHForWzInbound(index, applyNow = true) { + const st = document.getElementById("wzStatus"); + const source = wzInbounds[index]; + const selectedID = selectedXrayServer() || "local"; + if (!source || !visualXHTTPSettings(source) || !["vless", "vmess"].includes(String(source.protocol || "").toLowerCase())) { + if (st) st.textContent = "Selecione um inbound VLESS/VMess com transporte XHTTP."; + return false; + } + if (!wzLoadedConfigText || String(wzLoadedServerID || "") !== String(selectedID)) { + if (st) st.textContent = "Carregue a configuração do servidor selecionado antes de ativar SSH."; + return false; + } + if ((document.getElementById("xCoreMode")?.value || "native") !== "native") { + if (st) st.textContent = "SSH compartilhado requer o modo Xray nativo."; + return false; + } + const security = String(source.streamSettings?.security || "").toLowerCase(); + if (security && security !== "none" && security !== "tls") { + if (st) st.textContent = `O inbound ${source.tag || "selecionado"} usa ${security}, que não é suportado pelo SSH XHTTP nativo. Use TLS ou sem TLS.`; + return false; + } + const existingSSH = findSSHRouteForInbound(source); + if (existingSSH) { + if (st) st.textContent = `SSH já está ativado em /ssh pelo inbound ${existingSSH.tag}.`; + return true; + } + const pathConflict = wzInbounds.find(candidate => { + const xh = visualXHTTPSettings(candidate); + return sameVisualEndpoint(source, candidate) && !!xh && normalizeVisualPath(xh.path) === "/ssh"; + }); + if (pathConflict) { + if (st) st.textContent = `O caminho /ssh já é usado pelo inbound ${pathConflict.tag || "sem tag"}. Edite esse caminho primeiro.`; + return false; + } + if (security === "tls") { + const certificate = source.streamSettings?.tlsSettings?.certificates?.[0] || {}; + if (!certificate.certificateFile || !certificate.keyFile) { + if (st) st.textContent = "O inbound usa TLS, mas não possui arquivos de certificado e chave reutilizáveis."; + return false; + } + } + if (applyNow && !confirm(`Ativar SSH em /ssh no mesmo endpoint de ${source.tag || "este inbound"}?\n\nA configuração antiga e todos os clientes serão mantidos. O Xray será reiniciado para aplicar.`)) return false; + + const streamSettings = cloneJsonSafe(source.streamSettings || {}); + const settingsKey = streamSettings.xhttpSettings ? "xhttpSettings" : (streamSettings.splithttpSettings ? "splithttpSettings" : "xhttpSettings"); + streamSettings[settingsKey] = Object.assign({}, streamSettings[settingsKey] || {}, { path:"/ssh" }); + const sshInbound = { + tag: uniqueLegacySSHTag(source), + listen: source.listen || "0.0.0.0", + port: cloneJsonSafe(source.port), + protocol: "ssh", + settings: {}, + streamSettings, + }; + const before = cloneJsonSafe(source); + wzInbounds.push(sshInbound); + try { + validateVisualInbounds(wzInbounds); + } catch (error) { + wzInbounds.pop(); + if (st) st.textContent = `Não foi possível ativar SSH: ${error.message}`; + return false; + } + if (JSON.stringify(source) !== JSON.stringify(before)) { + wzInbounds.pop(); + if (st) st.textContent = "A migração foi cancelada porque alteraria o inbound antigo."; + return false; + } + wzDirty = true; + renderWzInbounds(); + if (!applyNow) { + if (st) st.textContent = `SSH /ssh adicionado ao rascunho sem alterar ${source.tag || "o inbound antigo"}.`; + return true; + } + if (st) st.textContent = `Ativando SSH /ssh sem alterar ${source.tag || "o inbound antigo"}…`; + return await applyWizardConfig(); +} + function validateVisualInbounds(inbounds) { const tags = new Set(); const binds = new Map(); @@ -752,7 +866,7 @@ async function applyWizardConfig() { if (String(wzLoadedServerID || "") !== String(selectedID) || !wzLoadedConfigText) { if (st) st.textContent = `Reloading config from ${target} before saving...`; loadWizardFromConfig(); - return; + return false; } let cfg; @@ -760,7 +874,7 @@ async function applyWizardConfig() { cfg = buildConfigFromVisualEditor(); } catch(e) { if (st) st.textContent = `Invalid visual config: ${e.message}`; - return; + return false; } if (st) st.textContent = `Saving config to ${target}...`; @@ -778,8 +892,10 @@ async function applyWizardConfig() { ? `Config saved on ${target} and Xray restarted.` : `Config saved on ${target}, but Xray could not restart. Check Xray logs before editing again.`; setTimeout(() => { loadXrayStatus(); loadInbounds({ force: true }); }, 700); + return !!restarted; } catch (e) { if (e.message==="auth") doAuthError(); else if (st) st.textContent = "Error: " + e.message; + return false; } } diff --git a/admin/index.html b/admin/index.html index 6955e82..a5b1409 100644 --- a/admin/index.html +++ b/admin/index.html @@ -16,7 +16,7 @@ setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500); })(); - +
@@ -428,6 +428,14 @@
Carregue a configuração para detectar um endpoint existente.
+ +
@@ -1437,17 +1445,17 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/xray_native_test.go b/xray_native_test.go index 2ddef32..b1222f6 100644 --- a/xray_native_test.go +++ b/xray_native_test.go @@ -50,7 +50,7 @@ func TestSharedXHTTPListenerRejectsDuplicatePath(t *testing.T) { func TestValidateNativeSharedXHTTPConfig(t *testing.T) { valid := []byte(`{"inbounds":[ - {"tag":"proxy","listen":"0.0.0.0","port":443,"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"path":"/"}}}, + {"tag":"legacy-proxy","listen":"0.0.0.0","port":443,"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"path":"/legacy"}}}, {"tag":"ssh","listen":"0.0.0.0","port":443,"protocol":"ssh","streamSettings":{"network":"xhttp","xhttpSettings":{"path":"/ssh"}}} ]}`) if err := validateNativeInboundBindings(valid); err != nil {