XHTTP SSH
This commit is contained in:
@@ -143,23 +143,40 @@ function wzToggleAddInbound() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWzProtoChange(val) {
|
function onWzProtoChange(val) {
|
||||||
const usesClientTransport = val === "vless" || val === "vmess";
|
const isSSH = val === "ssh";
|
||||||
document.getElementById("wzVlessFields").style.display = usesClientTransport ? "grid" : "none";
|
// SSH tunnels reuse the VLESS/VMess transport block to expose the XHTTP
|
||||||
|
// fields, but carry no proxy client list of their own.
|
||||||
|
const usesTransportFields = val === "vless" || val === "vmess" || isSSH;
|
||||||
|
document.getElementById("wzVlessFields").style.display = usesTransportFields ? "grid" : "none";
|
||||||
document.getElementById("wzTrojanFields").style.display = val === "trojan" ? "" : "none";
|
document.getElementById("wzTrojanFields").style.display = val === "trojan" ? "" : "none";
|
||||||
document.getElementById("wzSSFields").style.display = val === "shadowsocks" ? "grid" : "none";
|
document.getElementById("wzSSFields").style.display = val === "shadowsocks" ? "grid" : "none";
|
||||||
|
|
||||||
|
// SSH runs only over XHTTP: force the network to xhttp and lock the dropdown
|
||||||
|
// so the wizard can only emit a valid xhttp+ssh inbound.
|
||||||
|
const netSel = document.getElementById("wzNetwork");
|
||||||
|
if (isSSH) {
|
||||||
|
netSel.value = "xhttp";
|
||||||
|
netSel.disabled = true;
|
||||||
|
onWzNetworkChange("xhttp");
|
||||||
|
} else {
|
||||||
|
netSel.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
const tlsSel = document.getElementById("wzTLS");
|
const tlsSel = document.getElementById("wzTLS");
|
||||||
const realityOpt = document.querySelector("#wzTLS option[value='reality']");
|
const realityOpt = document.querySelector("#wzTLS option[value='reality']");
|
||||||
if (realityOpt) {
|
if (realityOpt) {
|
||||||
realityOpt.disabled = val === "vmess";
|
// REALITY is not wired for the native XHTTP listener (tls/none only) and is
|
||||||
if (val === "vmess" && tlsSel.value === "reality") {
|
// unavailable for VMess, so disable it for both.
|
||||||
|
const noReality = val === "vmess" || isSSH;
|
||||||
|
realityOpt.disabled = noReality;
|
||||||
|
if (noReality && tlsSel.value === "reality") {
|
||||||
tlsSel.value = "none";
|
tlsSel.value = "none";
|
||||||
onWzTLSChange("none");
|
onWzTLSChange("none");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const portMap = { vless:10086, vmess:10087, trojan:8443, shadowsocks:8388, socks:10808 };
|
const portMap = { vless:10086, vmess:10087, ssh:2087, trojan:8443, shadowsocks:8388, socks:10808 };
|
||||||
const tagMap = { vless:"vless-in", vmess:"vmess-in", trojan:"trojan-in", shadowsocks:"ss-in", socks:"socks-local" };
|
const tagMap = { vless:"vless-in", vmess:"vmess-in", ssh:"ssh-xhttp-in", trojan:"trojan-in", shadowsocks:"ss-in", socks:"socks-local" };
|
||||||
const portEl = document.getElementById("wzPort");
|
const portEl = document.getElementById("wzPort");
|
||||||
const tagEl = document.getElementById("wzTag");
|
const tagEl = document.getElementById("wzTag");
|
||||||
const lisEl = document.getElementById("wzListenIP");
|
const lisEl = document.getElementById("wzListenIP");
|
||||||
@@ -265,6 +282,24 @@ function wzSaveInbound() {
|
|||||||
shortIds: [document.getElementById("wzRealityShortID").value.trim()].filter(Boolean),
|
shortIds: [document.getElementById("wzRealityShortID").value.trim()].filter(Boolean),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (proto === "ssh") {
|
||||||
|
// SSH tunnel over XHTTP: no proxy clients — the decoded stream is handed to
|
||||||
|
// the SSH server, so authentication is an ordinary SSH account.
|
||||||
|
ib.settings = {};
|
||||||
|
ib.streamSettings = { network: "xhttp" };
|
||||||
|
ib.streamSettings.xhttpSettings = {
|
||||||
|
path: document.getElementById("wzXHTTPPath").value.trim() || "/xhttp",
|
||||||
|
host: document.getElementById("wzXHTTPHost").value.trim() || undefined,
|
||||||
|
mode: document.getElementById("wzXHTTPMode").value,
|
||||||
|
};
|
||||||
|
if (!ib.streamSettings.xhttpSettings.host) delete ib.streamSettings.xhttpSettings.host;
|
||||||
|
const tlsVal = document.getElementById("wzTLS").value;
|
||||||
|
if (tlsVal === "tls") {
|
||||||
|
ib.streamSettings.security = "tls";
|
||||||
|
ib.streamSettings.tlsSettings = {
|
||||||
|
certificates: [{ certificateFile: document.getElementById("wzTLSCert").value.trim(), keyFile: document.getElementById("wzTLSKey").value.trim() }],
|
||||||
|
};
|
||||||
|
}
|
||||||
} else if (proto === "trojan") {
|
} else if (proto === "trojan") {
|
||||||
ib.settings = { clients: [{ password: document.getElementById("wzTrojanPass").value.trim() || "change-me" }] };
|
ib.settings = { clients: [{ password: document.getElementById("wzTrojanPass").value.trim() || "change-me" }] };
|
||||||
ib.streamSettings = { network: "tcp", security: "tls", tlsSettings: {} };
|
ib.streamSettings = { network: "tcp", security: "tls", tlsSettings: {} };
|
||||||
|
|||||||
@@ -397,6 +397,7 @@
|
|||||||
<select id="wzProtocol" onchange="onWzProtoChange(this.value)">
|
<select id="wzProtocol" onchange="onWzProtoChange(this.value)">
|
||||||
<option value="vless">VLESS</option>
|
<option value="vless">VLESS</option>
|
||||||
<option value="vmess">VMess</option>
|
<option value="vmess">VMess</option>
|
||||||
|
<option value="ssh">SSH Tunnel (over XHTTP)</option>
|
||||||
<option value="trojan">Trojan</option>
|
<option value="trojan">Trojan</option>
|
||||||
<option value="shadowsocks">Shadowsocks</option>
|
<option value="shadowsocks">Shadowsocks</option>
|
||||||
<option value="socks">SOCKS5 (local)</option>
|
<option value="socks">SOCKS5 (local)</option>
|
||||||
|
|||||||
+19
-2
@@ -62,7 +62,7 @@ type nativeXrayClient struct {
|
|||||||
// nativeInbound is a single listener built from one JSON inbound entry.
|
// nativeInbound is a single listener built from one JSON inbound entry.
|
||||||
type nativeInbound struct {
|
type nativeInbound struct {
|
||||||
tag string
|
tag string
|
||||||
protocol string // "vless" | "vmess"
|
protocol string // "vless" | "vmess" | "ssh" (XHTTP->SSH tunnel)
|
||||||
listen string // bind host, default 0.0.0.0
|
listen string // bind host, default 0.0.0.0
|
||||||
port int
|
port int
|
||||||
transport string // "tcp" | "ws" | "xhttp" | ...
|
transport string // "tcp" | "ws" | "xhttp" | ...
|
||||||
@@ -959,7 +959,11 @@ func parseNativeInbounds(configFile string) ([]*nativeInbound, error) {
|
|||||||
var out []*nativeInbound
|
var out []*nativeInbound
|
||||||
for _, in := range cf.Inbounds {
|
for _, in := range cf.Inbounds {
|
||||||
proto := strings.ToLower(strings.TrimSpace(in.Protocol))
|
proto := strings.ToLower(strings.TrimSpace(in.Protocol))
|
||||||
if !xrayClientProtos[proto] {
|
// "ssh" is a DragonCore extension: an XHTTP inbound whose decoded byte
|
||||||
|
// stream is handed to the SSH server (handleConn) instead of a proxy
|
||||||
|
// protocol. It carries no proxy clients (auth is the SSH account), so it
|
||||||
|
// intentionally bypasses the client-bearing protocol gate below.
|
||||||
|
if proto != "ssh" && !xrayClientProtos[proto] {
|
||||||
continue // only vless/vmess/trojan carry clients; skip api/freedom/etc.
|
continue // only vless/vmess/trojan carry clients; skip api/freedom/etc.
|
||||||
}
|
}
|
||||||
port, ok := parseSinglePort(in.Port)
|
port, ok := parseSinglePort(in.Port)
|
||||||
@@ -1021,6 +1025,19 @@ func parseNativeInbounds(configFile string) ([]*nativeInbound, error) {
|
|||||||
ib.tlsConfig = tc
|
ib.tlsConfig = tc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XHTTP->SSH inbounds have no proxy clients: the SSH handshake performed by
|
||||||
|
// handleConn is the authentication step. Skip proxy-client loading and the
|
||||||
|
// clientCount()==0 gate below, but enforce that "ssh" is only valid on the
|
||||||
|
// XHTTP transport (that is the only path that reaches dispatchXHTTPConn).
|
||||||
|
if ib.protocol == "ssh" {
|
||||||
|
if !ib.isXHTTP() {
|
||||||
|
xrayLogf("native xray: inbound %q protocol \"ssh\" requires xhttp transport; skipping", in.Tag)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, ib)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
configClients := in.Settings.Clients
|
configClients := in.Settings.Clients
|
||||||
if len(in.Settings.Users) > 0 {
|
if len(in.Settings.Users) > 0 {
|
||||||
configClients = append(configClients, in.Settings.Users...)
|
configClients = append(configClients, in.Settings.Users...)
|
||||||
|
|||||||
@@ -728,6 +728,17 @@ func (ib *nativeInbound) dispatchXHTTPConn(xc net.Conn, remote net.Addr) {
|
|||||||
ib.handleVLESS(xc, remote)
|
ib.handleVLESS(xc, remote)
|
||||||
case "vmess":
|
case "vmess":
|
||||||
ib.handleVMess(xc, remote)
|
ib.handleVMess(xc, remote)
|
||||||
|
case "ssh":
|
||||||
|
// XHTTP->SSH tunnel: the decoded stream is a raw SSH transport. Hand it to
|
||||||
|
// the same SSH handler the TLS/DNSTT listeners use so tunneled clients
|
||||||
|
// authenticate with ordinary SSH accounts. getSSHConfig() is the live,
|
||||||
|
// hot-reloadable config; it can be nil only before the SSH server is set up.
|
||||||
|
cfg := getSSHConfig()
|
||||||
|
if cfg == nil {
|
||||||
|
xrayLogf("native xray: inbound %q XHTTP->SSH has no SSH config available yet", ib.tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleConn(xc, cfg)
|
||||||
default:
|
default:
|
||||||
xrayLogf("native xray: inbound %q XHTTP protocol %q not supported", ib.tag, ib.protocol)
|
xrayLogf("native xray: inbound %q XHTTP protocol %q not supported", ib.tag, ib.protocol)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user