Fix xray
This commit is contained in:
@@ -2434,6 +2434,86 @@ function toggleUdpgwFields(on) {
|
|||||||
el.style.pointerEvents = on ? "" : "none";
|
el.style.pointerEvents = on ? "" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const XRAY_NATIVE_TUNING_DEFAULTS = {
|
||||||
|
safe: {
|
||||||
|
mux_max_sessions: 64,
|
||||||
|
mux_global_sessions: 8192,
|
||||||
|
mux_udp_idle_ms: 30000,
|
||||||
|
mux_udp_read_buffer: 131072,
|
||||||
|
mux_udp_write_buffer: 131072,
|
||||||
|
xhttp_max_sessions: 4096,
|
||||||
|
xhttp_buffered_posts: 32,
|
||||||
|
xhttp_queue_timeout_ms: 500,
|
||||||
|
xhttp_flush_ms: 2,
|
||||||
|
xhttp_flush_bytes: 32768,
|
||||||
|
h2_max_concurrent_streams: 256,
|
||||||
|
h2_upload_buffer_conn: 1048576,
|
||||||
|
h2_upload_buffer_stream: 262144,
|
||||||
|
trace_packets: false,
|
||||||
|
},
|
||||||
|
"2k": {
|
||||||
|
mux_max_sessions: 128,
|
||||||
|
mux_global_sessions: 32768,
|
||||||
|
mux_udp_idle_ms: 15000,
|
||||||
|
mux_udp_read_buffer: 262144,
|
||||||
|
mux_udp_write_buffer: 262144,
|
||||||
|
xhttp_max_sessions: 16384,
|
||||||
|
xhttp_buffered_posts: 64,
|
||||||
|
xhttp_queue_timeout_ms: 250,
|
||||||
|
xhttp_flush_ms: 5,
|
||||||
|
xhttp_flush_bytes: 131072,
|
||||||
|
h2_max_concurrent_streams: 1024,
|
||||||
|
h2_upload_buffer_conn: 1048576,
|
||||||
|
h2_upload_buffer_stream: 262144,
|
||||||
|
trace_packets: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const XRAY_NATIVE_TUNING_FIELDS = {
|
||||||
|
mux_max_sessions: "cfgXrayMuxMaxSessions",
|
||||||
|
mux_global_sessions: "cfgXrayMuxGlobalSessions",
|
||||||
|
mux_udp_idle_ms: "cfgXrayMuxUdpIdleMs",
|
||||||
|
mux_udp_read_buffer: "cfgXrayMuxUdpRbuf",
|
||||||
|
mux_udp_write_buffer: "cfgXrayMuxUdpWbuf",
|
||||||
|
xhttp_max_sessions: "cfgXrayXhttpMaxSessions",
|
||||||
|
xhttp_buffered_posts: "cfgXrayXhttpBufferedPosts",
|
||||||
|
xhttp_queue_timeout_ms: "cfgXrayXhttpQueueTimeoutMs",
|
||||||
|
xhttp_flush_ms: "cfgXrayXhttpFlushMs",
|
||||||
|
xhttp_flush_bytes: "cfgXrayXhttpFlushBytes",
|
||||||
|
h2_max_concurrent_streams: "cfgXrayH2MaxStreams",
|
||||||
|
h2_upload_buffer_conn: "cfgXrayH2UploadConn",
|
||||||
|
h2_upload_buffer_stream: "cfgXrayH2UploadStream",
|
||||||
|
};
|
||||||
|
|
||||||
|
function setXrayNativeTuningDefaults(profile = "2k") {
|
||||||
|
const t = XRAY_NATIVE_TUNING_DEFAULTS[profile] || XRAY_NATIVE_TUNING_DEFAULTS.safe;
|
||||||
|
writeXrayNativeTuning(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeXrayNativeTuning(t = {}) {
|
||||||
|
const defaults = XRAY_NATIVE_TUNING_DEFAULTS.safe;
|
||||||
|
Object.entries(XRAY_NATIVE_TUNING_FIELDS).forEach(([key, id]) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = Number(t[key] || defaults[key] || 0);
|
||||||
|
});
|
||||||
|
const trace = document.getElementById("cfgXrayTracePackets");
|
||||||
|
if (trace) trace.checked = !!t.trace_packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readXrayNativeTuning() {
|
||||||
|
const out = {};
|
||||||
|
Object.entries(XRAY_NATIVE_TUNING_FIELDS).forEach(([key, id]) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
out[key] = parseInt(el?.value || "0", 10) || 0;
|
||||||
|
});
|
||||||
|
out.trace_packets = !!document.getElementById("cfgXrayTracePackets")?.checked;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline onclick handlers in index.html need this on window when the bundled JS is served from admin/assets/app.js.
|
||||||
|
window.setXrayNativeTuningDefaults = setXrayNativeTuningDefaults;
|
||||||
|
|
||||||
async function loadServerConfig() {
|
async function loadServerConfig() {
|
||||||
const st = document.getElementById("srvCfgStatus");
|
const st = document.getElementById("srvCfgStatus");
|
||||||
st.textContent = "Loading…";
|
st.textContent = "Loading…";
|
||||||
@@ -2503,6 +2583,7 @@ async function loadServerConfig() {
|
|||||||
const x = c.xray || {};
|
const x = c.xray || {};
|
||||||
document.getElementById("cfgXrayEnabled").checked = !!x.enabled;
|
document.getElementById("cfgXrayEnabled").checked = !!x.enabled;
|
||||||
document.getElementById("cfgXrayMode").value = xrayModeFromConfig(x);
|
document.getElementById("cfgXrayMode").value = xrayModeFromConfig(x);
|
||||||
|
writeXrayNativeTuning(x.native_tuning || XRAY_NATIVE_TUNING_DEFAULTS.safe);
|
||||||
|
|
||||||
st.textContent = "Config loaded.";
|
st.textContent = "Config loaded.";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -2576,6 +2657,7 @@ async function saveServerConfig() {
|
|||||||
api_server: "127.0.0.1:10085",
|
api_server: "127.0.0.1:10085",
|
||||||
online_window_seconds: 90,
|
online_window_seconds: 90,
|
||||||
stats_poll_seconds: 15,
|
stats_poll_seconds: 15,
|
||||||
|
native_tuning: readXrayNativeTuning(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+25
-1
@@ -1188,6 +1188,30 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="field" style="margin-top:8px;"><label>Runtime mode</label><select id="cfgXrayMode"><option value="native">Internal native emulator</option><option value="external">External xray binary</option></select></div>
|
<div class="field" style="margin-top:8px;"><label>Runtime mode</label><select id="cfgXrayMode"><option value="native">Internal native emulator</option><option value="external">External xray binary</option></select></div>
|
||||||
<div class="hint" style="margin-top:6px;color:var(--muted);">Native runs inside DragonCoreSSH. External uses /opt/sshpanel/xray with DB-backed /opt/sshpanel/xray_config.json and Stats API on 127.0.0.1:10085.</div>
|
<div class="hint" style="margin-top:6px;color:var(--muted);">Native runs inside DragonCoreSSH. External uses /opt/sshpanel/xray with DB-backed /opt/sshpanel/xray_config.json and Stats API on 127.0.0.1:10085.</div>
|
||||||
|
<details style="margin-top:10px;" open>
|
||||||
|
<summary style="cursor:pointer;font-size:.76rem;font-weight:700;color:var(--text);">Native Xray scale tuning</summary>
|
||||||
|
<div class="grid2" style="margin-top:10px;gap:8px;">
|
||||||
|
<div class="field"><label>Mux sessions per connection</label><input type="number" min="1" id="cfgXrayMuxMaxSessions" placeholder="128"/></div>
|
||||||
|
<div class="field"><label>Global mux backend sessions</label><input type="number" min="1" id="cfgXrayMuxGlobalSessions" placeholder="32768"/></div>
|
||||||
|
<div class="field"><label>Mux UDP idle ms</label><input type="number" min="1000" id="cfgXrayMuxUdpIdleMs" placeholder="15000"/></div>
|
||||||
|
<div class="field"><label>Mux UDP read buffer bytes</label><input type="number" min="4096" id="cfgXrayMuxUdpRbuf" placeholder="262144"/></div>
|
||||||
|
<div class="field"><label>Mux UDP write buffer bytes</label><input type="number" min="4096" id="cfgXrayMuxUdpWbuf" placeholder="262144"/></div>
|
||||||
|
<div class="field"><label>XHTTP max active sessions</label><input type="number" min="1" id="cfgXrayXhttpMaxSessions" placeholder="16384"/></div>
|
||||||
|
<div class="field"><label>XHTTP buffered posts</label><input type="number" min="1" id="cfgXrayXhttpBufferedPosts" placeholder="64"/></div>
|
||||||
|
<div class="field"><label>XHTTP queue timeout ms</label><input type="number" min="10" id="cfgXrayXhttpQueueTimeoutMs" placeholder="250"/></div>
|
||||||
|
<div class="field"><label>XHTTP flush ms</label><input type="number" min="1" id="cfgXrayXhttpFlushMs" placeholder="5"/></div>
|
||||||
|
<div class="field"><label>XHTTP flush bytes</label><input type="number" min="1024" id="cfgXrayXhttpFlushBytes" placeholder="131072"/></div>
|
||||||
|
<div class="field"><label>HTTP/2 max streams</label><input type="number" min="1" id="cfgXrayH2MaxStreams" placeholder="1024"/></div>
|
||||||
|
<div class="field"><label>HTTP/2 upload buffer / conn</label><input type="number" min="65536" id="cfgXrayH2UploadConn" placeholder="1048576"/></div>
|
||||||
|
<div class="field"><label>HTTP/2 upload buffer / stream</label><input type="number" min="32768" id="cfgXrayH2UploadStream" placeholder="262144"/></div>
|
||||||
|
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;grid-column:1/-1"><input type="checkbox" id="cfgXrayTracePackets"/> Trace every XHTTP/mux packet <span class="hint">debug only, slows QUIC</span></label>
|
||||||
|
<div class="card-actions" style="grid-column:1/-1;">
|
||||||
|
<button class="btn btn-ghost btn-sm" type="button" onclick="setXrayNativeTuningDefaults('2k')">Apply 2K defaults</button>
|
||||||
|
<button class="btn btn-ghost btn-sm" type="button" onclick="setXrayNativeTuningDefaults('safe')">Apply safe defaults</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint" style="grid-column:1/-1;margin-top:-4px;">These values are saved in the panel config and applied live on restart/reload. No systemd Environment lines are needed.</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /right -->
|
</div><!-- /right -->
|
||||||
@@ -1211,6 +1235,6 @@
|
|||||||
</div><!-- /shell -->
|
</div><!-- /shell -->
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
|
|
||||||
<script defer src="assets/app.js?v=20260622dnsttfakednsfast"></script>
|
<script defer src="assets/app.js?v=20260704xrayadmintune2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1143,6 +1143,85 @@ function toggleUdpgwFields(on) {
|
|||||||
el.style.pointerEvents = on ? "" : "none";
|
el.style.pointerEvents = on ? "" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const XRAY_NATIVE_TUNING_DEFAULTS = {
|
||||||
|
safe: {
|
||||||
|
mux_max_sessions: 128,
|
||||||
|
mux_global_sessions: 32768,
|
||||||
|
mux_udp_idle_ms: 15000,
|
||||||
|
mux_udp_read_buffer: 262144,
|
||||||
|
mux_udp_write_buffer: 262144,
|
||||||
|
xhttp_max_sessions: 16384,
|
||||||
|
xhttp_buffered_posts: 64,
|
||||||
|
xhttp_queue_timeout_ms: 250,
|
||||||
|
xhttp_flush_ms: 5,
|
||||||
|
xhttp_flush_bytes: 131072,
|
||||||
|
h2_max_concurrent_streams: 1024,
|
||||||
|
h2_upload_buffer_conn: 1048576,
|
||||||
|
h2_upload_buffer_stream: 262144,
|
||||||
|
trace_packets: false,
|
||||||
|
},
|
||||||
|
"2k": {
|
||||||
|
mux_max_sessions: 128,
|
||||||
|
mux_global_sessions: 32768,
|
||||||
|
mux_udp_idle_ms: 15000,
|
||||||
|
mux_udp_read_buffer: 262144,
|
||||||
|
mux_udp_write_buffer: 262144,
|
||||||
|
xhttp_max_sessions: 16384,
|
||||||
|
xhttp_buffered_posts: 64,
|
||||||
|
xhttp_queue_timeout_ms: 250,
|
||||||
|
xhttp_flush_ms: 5,
|
||||||
|
xhttp_flush_bytes: 131072,
|
||||||
|
h2_max_concurrent_streams: 1024,
|
||||||
|
h2_upload_buffer_conn: 1048576,
|
||||||
|
h2_upload_buffer_stream: 262144,
|
||||||
|
trace_packets: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const XRAY_NATIVE_TUNING_FIELDS = {
|
||||||
|
mux_max_sessions: "cfgXrayMuxMaxSessions",
|
||||||
|
mux_global_sessions: "cfgXrayMuxGlobalSessions",
|
||||||
|
mux_udp_idle_ms: "cfgXrayMuxUdpIdleMs",
|
||||||
|
mux_udp_read_buffer: "cfgXrayMuxUdpRbuf",
|
||||||
|
mux_udp_write_buffer: "cfgXrayMuxUdpWbuf",
|
||||||
|
xhttp_max_sessions: "cfgXrayXhttpMaxSessions",
|
||||||
|
xhttp_buffered_posts: "cfgXrayXhttpBufferedPosts",
|
||||||
|
xhttp_queue_timeout_ms: "cfgXrayXhttpQueueTimeoutMs",
|
||||||
|
xhttp_flush_ms: "cfgXrayXhttpFlushMs",
|
||||||
|
xhttp_flush_bytes: "cfgXrayXhttpFlushBytes",
|
||||||
|
h2_max_concurrent_streams: "cfgXrayH2MaxStreams",
|
||||||
|
h2_upload_buffer_conn: "cfgXrayH2UploadConn",
|
||||||
|
h2_upload_buffer_stream: "cfgXrayH2UploadStream",
|
||||||
|
};
|
||||||
|
|
||||||
|
function setXrayNativeTuningDefaults(profile = "2k") {
|
||||||
|
const t = XRAY_NATIVE_TUNING_DEFAULTS[profile] || XRAY_NATIVE_TUNING_DEFAULTS.safe;
|
||||||
|
writeXrayNativeTuning(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeXrayNativeTuning(t = {}) {
|
||||||
|
const defaults = XRAY_NATIVE_TUNING_DEFAULTS.safe;
|
||||||
|
Object.entries(XRAY_NATIVE_TUNING_FIELDS).forEach(([key, id]) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = Number(t[key] || defaults[key] || 0);
|
||||||
|
});
|
||||||
|
const trace = document.getElementById("cfgXrayTracePackets");
|
||||||
|
if (trace) trace.checked = !!t.trace_packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readXrayNativeTuning() {
|
||||||
|
const out = {};
|
||||||
|
Object.entries(XRAY_NATIVE_TUNING_FIELDS).forEach(([key, id]) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
out[key] = parseInt(el?.value || "0", 10) || 0;
|
||||||
|
});
|
||||||
|
out.trace_packets = !!document.getElementById("cfgXrayTracePackets")?.checked;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setXrayNativeTuningDefaults = setXrayNativeTuningDefaults;
|
||||||
|
|
||||||
async function loadServerConfig() {
|
async function loadServerConfig() {
|
||||||
const st = document.getElementById("srvCfgStatus");
|
const st = document.getElementById("srvCfgStatus");
|
||||||
st.textContent = "Loading…";
|
st.textContent = "Loading…";
|
||||||
@@ -1211,6 +1290,8 @@ async function loadServerConfig() {
|
|||||||
// Xray
|
// Xray
|
||||||
const x = c.xray || {};
|
const x = c.xray || {};
|
||||||
document.getElementById("cfgXrayEnabled").checked = !!x.enabled;
|
document.getElementById("cfgXrayEnabled").checked = !!x.enabled;
|
||||||
|
if (document.getElementById("cfgXrayMode")) document.getElementById("cfgXrayMode").value = x.mode || (x.native === false ? "external" : "native");
|
||||||
|
writeXrayNativeTuning(x.native_tuning || XRAY_NATIVE_TUNING_DEFAULTS.safe);
|
||||||
|
|
||||||
st.textContent = "Config loaded.";
|
st.textContent = "Config loaded.";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1276,8 +1357,12 @@ async function saveServerConfig() {
|
|||||||
tls_forwarders: tlsArr,
|
tls_forwarders: tlsArr,
|
||||||
xray: {
|
xray: {
|
||||||
enabled: document.getElementById("cfgXrayEnabled").checked,
|
enabled: document.getElementById("cfgXrayEnabled").checked,
|
||||||
|
mode: document.getElementById("cfgXrayMode")?.value || "native",
|
||||||
|
native: (document.getElementById("cfgXrayMode")?.value || "native") !== "external",
|
||||||
bin_path: "/opt/sshpanel/xray",
|
bin_path: "/opt/sshpanel/xray",
|
||||||
config_file: "/opt/sshpanel/xray_config.json",
|
config_file: "/opt/sshpanel/xray_config.json",
|
||||||
|
native_config_file: "/opt/sshpanel/xray_native_config.json",
|
||||||
|
native_tuning: readXrayNativeTuning(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,19 @@ module shell2
|
|||||||
|
|
||||||
go 1.25.4
|
go 1.25.4
|
||||||
|
|
||||||
require (
|
require golang.org/x/crypto v0.45.0
|
||||||
github.com/lib/pq v1.10.9
|
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.61
|
|
||||||
github.com/xtaci/smux v1.5.50
|
|
||||||
golang.org/x/crypto v0.45.0
|
|
||||||
golang.org/x/net v0.47.0
|
|
||||||
golang.org/x/time v0.14.0
|
|
||||||
www.bamsoftware.com/git/dnstt.git v1.20241021.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/flynn/noise v1.0.0 // indirect
|
github.com/flynn/noise v1.0.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
|
github.com/xtaci/kcp-go/v5 v5.6.61 // indirect
|
||||||
|
github.com/xtaci/smux v1.5.50 // indirect
|
||||||
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
www.bamsoftware.com/git/dnstt.git v1.20241021.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
@@ -29,26 +27,18 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4
|
|||||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
|
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
|
||||||
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
|
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.61 h1:ajm12pGuWO+GWQNusPyPESC7Rq0yTC2rEXVYkM8ExOg=
|
github.com/xtaci/kcp-go/v5 v5.6.61 h1:ajm12pGuWO+GWQNusPyPESC7Rq0yTC2rEXVYkM8ExOg=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.61/go.mod h1:9O3D8WR+cyyUjGiTILYfg17vn72otWuXK2AFfqIe6CM=
|
github.com/xtaci/kcp-go/v5 v5.6.61/go.mod h1:9O3D8WR+cyyUjGiTILYfg17vn72otWuXK2AFfqIe6CM=
|
||||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
|
|
||||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
|
|
||||||
github.com/xtaci/smux v1.5.50 h1:y/1DlWQC9bnMeZzsyk4oL2hbLK6uVk4BKTz5BeQqUEA=
|
github.com/xtaci/smux v1.5.50 h1:y/1DlWQC9bnMeZzsyk4oL2hbLK6uVk4BKTz5BeQqUEA=
|
||||||
github.com/xtaci/smux v1.5.50/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX825Q=
|
github.com/xtaci/smux v1.5.50/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX825Q=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
@@ -83,12 +73,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -111,10 +97,7 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
|
|||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
www.bamsoftware.com/git/dnstt.git v1.20241021.0 h1:Xi0lmT+5kcgzY7P+r726eBXKMZKgGoD8GTNKrlh8TuE=
|
www.bamsoftware.com/git/dnstt.git v1.20241021.0 h1:Xi0lmT+5kcgzY7P+r726eBXKMZKgGoD8GTNKrlh8TuE=
|
||||||
|
|||||||
+186
-32
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -49,6 +48,10 @@ type XrayConfig struct {
|
|||||||
// clients are preserved when the server has IPv6 connectivity. force_ipv4 is
|
// clients are preserved when the server has IPv6 connectivity. force_ipv4 is
|
||||||
// only kept as an explicit legacy override.
|
// only kept as an explicit legacy override.
|
||||||
NativeIPStrategy string `json:"native_ip_strategy,omitempty"` // auto | force_ipv4
|
NativeIPStrategy string `json:"native_ip_strategy,omitempty"` // auto | force_ipv4
|
||||||
|
|
||||||
|
// NativeTuning exposes native-emulator scale/performance knobs in the admin panel.
|
||||||
|
// These replace the old XRAY_NATIVE_* systemd environment overrides.
|
||||||
|
NativeTuning *XrayNativeTuning `json:"native_tuning,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -94,6 +97,9 @@ func (c *XrayConfig) NormalizeDefaults() {
|
|||||||
default:
|
default:
|
||||||
c.NativeIPStrategy = xrayNativeIPStrategyAuto
|
c.NativeIPStrategy = xrayNativeIPStrategyAuto
|
||||||
}
|
}
|
||||||
|
tuning := normalizeNativeXrayTuning(c.NativeTuning)
|
||||||
|
c.NativeTuning = &tuning
|
||||||
|
applyNativeXrayTuning(c.NativeTuning)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XrayConfig) NativeForceIPv4() bool {
|
func (c *XrayConfig) NativeForceIPv4() bool {
|
||||||
@@ -155,7 +161,12 @@ type xrayLogRing struct {
|
|||||||
pos int
|
pos int
|
||||||
}
|
}
|
||||||
|
|
||||||
const xrayLogCap = 200
|
const (
|
||||||
|
xrayLogCap = 5000
|
||||||
|
xrayForcedLogLevel = "debug"
|
||||||
|
xrayForcedAccessLogPath = "/dev/stdout"
|
||||||
|
xrayForcedErrorLogPath = "/dev/stderr"
|
||||||
|
)
|
||||||
|
|
||||||
func (r *xrayLogRing) add(line string) {
|
func (r *xrayLogRing) add(line string) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
@@ -200,6 +211,38 @@ func (w xrayWriter) Write(p []byte) (int, error) {
|
|||||||
return os.Stderr.Write(p)
|
return os.Stderr.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xrayLogf writes native/runtime Xray debug messages directly to stderr and
|
||||||
|
// to the in-memory Xray log ring. It intentionally bypasses the global
|
||||||
|
// standard logger because the panel can run in quiet mode and call
|
||||||
|
// log.SetOutput(io.Discard). Xray debug logs must remain visible while
|
||||||
|
// troubleshooting transport issues.
|
||||||
|
func xrayLogf(format string, args ...interface{}) {
|
||||||
|
msg := strings.TrimRight(fmt.Sprintf(format, args...), "\n")
|
||||||
|
if strings.TrimSpace(msg) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts := time.Now().Format("2006/01/02 15:04:05")
|
||||||
|
for _, line := range strings.Split(msg, "\n") {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
full := ts + " " + line
|
||||||
|
xrayLogBuf.add(full)
|
||||||
|
_, _ = fmt.Fprintln(os.Stderr, full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// xrayTracef is for very hot transport-level traces such as every XHTTP packet.
|
||||||
|
// Leaving those on under many QUIC users can bottleneck on journald/stderr and
|
||||||
|
// make the tunnel appear slow. Enable only when packet-level tracing is needed:
|
||||||
|
// Enable packet-level tracing from Admin Panel -> Xray Native Scale -> Trace packets.
|
||||||
|
func xrayTracef(format string, args ...interface{}) {
|
||||||
|
if !nativeTracePacketsEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xrayLogf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
// XrayManager manages the lifecycle of the external xray subprocess.
|
// XrayManager manages the lifecycle of the external xray subprocess.
|
||||||
type XrayManager struct {
|
type XrayManager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -249,7 +292,7 @@ func initXrayManager(cfg *XrayConfig) {
|
|||||||
xrayMgr.mu.Lock()
|
xrayMgr.mu.Lock()
|
||||||
xrayMgr.cfg = cfg
|
xrayMgr.cfg = cfg
|
||||||
if err := xrayMgr.bootstrapConfigStoreLocked(); err != nil {
|
if err := xrayMgr.bootstrapConfigStoreLocked(); err != nil {
|
||||||
log.Printf("xray: database config bootstrap failed: %v", err)
|
xrayLogf("xray: database config bootstrap failed: %v", err)
|
||||||
}
|
}
|
||||||
xrayMgr.mu.Unlock()
|
xrayMgr.mu.Unlock()
|
||||||
|
|
||||||
@@ -263,7 +306,7 @@ func initXrayManager(cfg *XrayConfig) {
|
|||||||
|
|
||||||
if cfg.Enabled {
|
if cfg.Enabled {
|
||||||
if err := xrayMgr.Start(); err != nil {
|
if err := xrayMgr.Start(); err != nil {
|
||||||
log.Printf("xray: auto-start failed: %v", err)
|
xrayLogf("xray: auto-start failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,11 +333,16 @@ func (m *XrayManager) Start() error {
|
|||||||
if m.cfg == nil {
|
if m.cfg == nil {
|
||||||
return fmt.Errorf("xray not configured")
|
return fmt.Errorf("xray not configured")
|
||||||
}
|
}
|
||||||
|
m.cfg.NormalizeDefaults()
|
||||||
if err := m.syncConfigFileFromStoreLocked(); err != nil {
|
if err := m.syncConfigFileFromStoreLocked(); err != nil {
|
||||||
m.lastErr = err.Error()
|
m.lastErr = err.Error()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configFile := m.activeConfigFileLocked()
|
configFile := m.activeConfigFileLocked()
|
||||||
|
if _, err := m.readConfigLocked(); err != nil && !os.IsNotExist(err) {
|
||||||
|
m.lastErr = err.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Native mode: run the in-process emulator instead of the subprocess.
|
// Native mode: run the in-process emulator instead of the subprocess.
|
||||||
if m.cfg.UseNative() {
|
if m.cfg.UseNative() {
|
||||||
@@ -319,7 +367,7 @@ func (m *XrayManager) Start() error {
|
|||||||
if changed, err := m.ensureStatsAPIConfigLocked(); err != nil {
|
if changed, err := m.ensureStatsAPIConfigLocked(); err != nil {
|
||||||
return fmt.Errorf("xray stats api check failed: %w", err)
|
return fmt.Errorf("xray stats api check failed: %w", err)
|
||||||
} else if changed {
|
} else if changed {
|
||||||
log.Printf("xray: repaired Stats API support in config before start")
|
xrayLogf("xray: repaired Stats API support and forced debug logs in config before start")
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"run"}
|
args := []string{"run"}
|
||||||
@@ -350,10 +398,10 @@ func (m *XrayManager) Start() error {
|
|||||||
m.lastErr = err.Error()
|
m.lastErr = err.Error()
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
log.Printf("xray: process exited: %v", err)
|
xrayLogf("xray: process exited: %v", err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Printf("xray: started (pid %d)", cmd.Process.Pid)
|
xrayLogf("xray: started (pid %d)", cmd.Process.Pid)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +434,7 @@ func (m *XrayManager) Stop() error {
|
|||||||
case <-time.After(2 * time.Second):
|
case <-time.After(2 * time.Second):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("xray: stopped")
|
xrayLogf("xray: stopped")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,13 +471,13 @@ func (m *XrayManager) recordNativeConnect(uuid, email string) {
|
|||||||
m.statsMu.Unlock()
|
m.statsMu.Unlock()
|
||||||
|
|
||||||
if statsStore != nil && uuid != "" {
|
if statsStore != nil && uuid != "" {
|
||||||
go func() {
|
xrayGo("native xray stats active increment", func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := statsStore.UpdateXrayClientActive(ctx, uuid, email, 1); err != nil {
|
if err := statsStore.UpdateXrayClientActive(ctx, uuid, email, 1); err != nil {
|
||||||
log.Printf("xray native stats: active +1 for %s failed: %v", uuid, err)
|
xrayLogf("xray native stats: active +1 for %s failed: %v", uuid, err)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,13 +501,13 @@ func (m *XrayManager) recordNativeDisconnect(uuid, email string) {
|
|||||||
m.statsMu.Unlock()
|
m.statsMu.Unlock()
|
||||||
|
|
||||||
if statsStore != nil && uuid != "" {
|
if statsStore != nil && uuid != "" {
|
||||||
go func() {
|
xrayGo("native xray stats active decrement", func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := statsStore.UpdateXrayClientActive(ctx, uuid, email, -1); err != nil {
|
if err := statsStore.UpdateXrayClientActive(ctx, uuid, email, -1); err != nil {
|
||||||
log.Printf("xray native stats: active -1 for %s failed: %v", uuid, err)
|
xrayLogf("xray native stats: active -1 for %s failed: %v", uuid, err)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,13 +559,13 @@ func (m *XrayManager) startNativeStatsFlusher() {
|
|||||||
m.nativeStatsFlushStarted = true
|
m.nativeStatsFlushStarted = true
|
||||||
m.nativeDBMu.Unlock()
|
m.nativeDBMu.Unlock()
|
||||||
|
|
||||||
go func() {
|
xrayGo("native xray stats flusher", func() {
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
m.flushNativeStatsToDB()
|
m.flushNativeStatsToDB()
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *XrayManager) flushNativeStatsToDB() {
|
func (m *XrayManager) flushNativeStatsToDB() {
|
||||||
@@ -534,7 +582,7 @@ func (m *XrayManager) flushNativeStatsToDB() {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := statsStore.AddXrayClientTrafficBatch(ctx, pending); err != nil {
|
if err := statsStore.AddXrayClientTrafficBatch(ctx, pending); err != nil {
|
||||||
log.Printf("xray native stats: db traffic flush failed: %v", err)
|
xrayLogf("xray native stats: db traffic flush failed: %v", err)
|
||||||
// Put deltas back so a transient DB failure does not lose accounting.
|
// Put deltas back so a transient DB failure does not lose accounting.
|
||||||
m.nativeDBMu.Lock()
|
m.nativeDBMu.Lock()
|
||||||
if m.nativeTrafficPending == nil {
|
if m.nativeTrafficPending == nil {
|
||||||
@@ -964,6 +1012,55 @@ func normalizeJSONIndent(data []byte) ([]byte, error) {
|
|||||||
return json.MarshalIndent(raw, "", " ")
|
return json.MarshalIndent(raw, "", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func forceXrayDebugLogBytes(data []byte) ([]byte, bool, error) {
|
||||||
|
if !json.Valid(data) {
|
||||||
|
return nil, false, fmt.Errorf("invalid JSON")
|
||||||
|
}
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("parse xray config: %w", err)
|
||||||
|
}
|
||||||
|
if raw == nil {
|
||||||
|
return nil, false, fmt.Errorf("xray config must be a JSON object")
|
||||||
|
}
|
||||||
|
changed := ensureXrayForcedDebugLogConfig(raw)
|
||||||
|
if !changed {
|
||||||
|
return data, false, nil
|
||||||
|
}
|
||||||
|
out, err := json.MarshalIndent(raw, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return out, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureXrayForcedDebugLogConfig(raw map[string]interface{}) bool {
|
||||||
|
changed := false
|
||||||
|
logObj := asObject(raw["log"])
|
||||||
|
if logObj == nil {
|
||||||
|
logObj = map[string]interface{}{}
|
||||||
|
raw["log"] = logObj
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if v, _ := logObj["access"].(string); v != xrayForcedAccessLogPath {
|
||||||
|
logObj["access"] = xrayForcedAccessLogPath
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if v, _ := logObj["error"].(string); v != xrayForcedErrorLogPath {
|
||||||
|
logObj["error"] = xrayForcedErrorLogPath
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if v, _ := logObj["loglevel"].(string); !strings.EqualFold(v, xrayForcedLogLevel) {
|
||||||
|
logObj["loglevel"] = xrayForcedLogLevel
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if v, _ := logObj["dnsLog"].(bool); !v {
|
||||||
|
logObj["dnsLog"] = true
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
func (m *XrayManager) readConfigLocked() ([]byte, error) {
|
func (m *XrayManager) readConfigLocked() ([]byte, error) {
|
||||||
configFile := m.activeConfigFileLocked()
|
configFile := m.activeConfigFileLocked()
|
||||||
if m.cfg == nil || configFile == "" {
|
if m.cfg == nil || configFile == "" {
|
||||||
@@ -971,8 +1068,15 @@ func (m *XrayManager) readConfigLocked() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
if statsStore != nil {
|
if statsStore != nil {
|
||||||
if data, ok, err := statsStore.GetXrayConfig(context.Background(), m.configStoreKeyLocked()); err != nil {
|
if data, ok, err := statsStore.GetXrayConfig(context.Background(), m.configStoreKeyLocked()); err != nil {
|
||||||
log.Printf("xray: database config read failed, falling back to file: %v", err)
|
xrayLogf("xray: database config read failed, falling back to file: %v", err)
|
||||||
} else if ok {
|
} else if ok {
|
||||||
|
if forced, changed, err := forceXrayDebugLogBytes(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if changed {
|
||||||
|
data = forced
|
||||||
|
_ = statsStore.UpsertXrayConfig(context.Background(), m.configStoreKeyLocked(), forced)
|
||||||
|
xrayLogf("xray: forced debug log output in database config")
|
||||||
|
}
|
||||||
pretty, err := normalizeJSONIndent(data)
|
pretty, err := normalizeJSONIndent(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -981,7 +1085,18 @@ func (m *XrayManager) readConfigLocked() ([]byte, error) {
|
|||||||
return pretty, nil
|
return pretty, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return os.ReadFile(configFile)
|
data, err := os.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if forced, changed, err := forceXrayDebugLogBytes(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if changed {
|
||||||
|
data = forced
|
||||||
|
_ = os.WriteFile(configFile, forced, 0o600)
|
||||||
|
xrayLogf("xray: forced debug log output in file config")
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *XrayManager) writeConfigLocked(data []byte) error {
|
func (m *XrayManager) writeConfigLocked(data []byte) error {
|
||||||
@@ -989,6 +1104,12 @@ func (m *XrayManager) writeConfigLocked(data []byte) error {
|
|||||||
if m.cfg == nil || configFile == "" {
|
if m.cfg == nil || configFile == "" {
|
||||||
return fmt.Errorf("xray config file not configured")
|
return fmt.Errorf("xray config file not configured")
|
||||||
}
|
}
|
||||||
|
if forced, changed, err := forceXrayDebugLogBytes(data); err != nil {
|
||||||
|
return err
|
||||||
|
} else if changed {
|
||||||
|
data = forced
|
||||||
|
xrayLogf("xray: forced debug log output while saving config")
|
||||||
|
}
|
||||||
pretty, err := normalizeJSONIndent(data)
|
pretty, err := normalizeJSONIndent(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1011,11 +1132,11 @@ func (m *XrayManager) importConfigClientsLocked(data []byte, source string) {
|
|||||||
}
|
}
|
||||||
n, err := statsStore.ImportXrayClientsFromConfig(context.Background(), data)
|
n, err := statsStore.ImportXrayClientsFromConfig(context.Background(), data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("xray: import clients from %s failed: %v", source, err)
|
xrayLogf("xray: import clients from %s failed: %v", source, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
log.Printf("xray: imported/synced %d client UUIDs from %s into database", n, source)
|
xrayLogf("xray: imported/synced %d client UUIDs from %s into database", n, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,13 +1148,13 @@ func (m *XrayManager) importRuntimeConfigFileClientsLocked(source string) {
|
|||||||
data, err := os.ReadFile(configFile)
|
data, err := os.ReadFile(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
log.Printf("xray: read %s for client import failed: %v", configFile, err)
|
xrayLogf("xray: read %s for client import failed: %v", configFile, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pretty, err := normalizeJSONIndent(data)
|
pretty, err := normalizeJSONIndent(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("xray: cannot import clients from %s: invalid JSON: %v", configFile, err)
|
xrayLogf("xray: cannot import clients from %s: invalid JSON: %v", configFile, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.importConfigClientsLocked(pretty, source)
|
m.importConfigClientsLocked(pretty, source)
|
||||||
@@ -1048,7 +1169,7 @@ func (m *XrayManager) restartIfExternalRunning() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := m.Restart(); err != nil {
|
if err := m.Restart(); err != nil {
|
||||||
log.Printf("xray: external restart after client/config change failed: %v", err)
|
xrayLogf("xray: external restart after client/config change failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,6 +1190,15 @@ func (m *XrayManager) bootstrapConfigStoreLocked() error {
|
|||||||
if data, ok, err := statsStore.GetXrayConfig(ctx, key); err != nil {
|
if data, ok, err := statsStore.GetXrayConfig(ctx, key); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
|
if forced, changed, err := forceXrayDebugLogBytes(data); err != nil {
|
||||||
|
return err
|
||||||
|
} else if changed {
|
||||||
|
data = forced
|
||||||
|
if err := statsStore.UpsertXrayConfig(ctx, key, forced); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
xrayLogf("xray: forced debug log output during config bootstrap")
|
||||||
|
}
|
||||||
pretty, err := normalizeJSONIndent(data)
|
pretty, err := normalizeJSONIndent(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1086,6 +1216,12 @@ func (m *XrayManager) bootstrapConfigStoreLocked() error {
|
|||||||
if migrated, migErr := m.seedNativeConfigFromExternalLocked(configFile); migErr != nil {
|
if migrated, migErr := m.seedNativeConfigFromExternalLocked(configFile); migErr != nil {
|
||||||
return migErr
|
return migErr
|
||||||
} else if len(migrated) > 0 {
|
} else if len(migrated) > 0 {
|
||||||
|
if forced, changed, err := forceXrayDebugLogBytes(migrated); err != nil {
|
||||||
|
return err
|
||||||
|
} else if changed {
|
||||||
|
migrated = forced
|
||||||
|
xrayLogf("xray: forced debug log output during native config migration")
|
||||||
|
}
|
||||||
pretty, err := normalizeJSONIndent(migrated)
|
pretty, err := normalizeJSONIndent(migrated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1102,6 +1238,12 @@ func (m *XrayManager) bootstrapConfigStoreLocked() error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if forced, changed, err := forceXrayDebugLogBytes(data); err != nil {
|
||||||
|
return err
|
||||||
|
} else if changed {
|
||||||
|
data = forced
|
||||||
|
xrayLogf("xray: forced debug log output while importing runtime config")
|
||||||
|
}
|
||||||
pretty, err := normalizeJSONIndent(data)
|
pretty, err := normalizeJSONIndent(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1128,7 +1270,7 @@ func (m *XrayManager) seedNativeConfigFromExternalLocked(nativeConfigFile string
|
|||||||
if _, err := normalizeJSONIndent(data); err != nil {
|
if _, err := normalizeJSONIndent(data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Printf("xray native: cloned %s to independent native config %s once", ext, nativeConfigFile)
|
xrayLogf("xray native: cloned %s to independent native config %s once", ext, nativeConfigFile)
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1141,6 +1283,15 @@ func (m *XrayManager) syncConfigFileFromStoreLocked() error {
|
|||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if forced, changed, err := forceXrayDebugLogBytes(data); err != nil {
|
||||||
|
return err
|
||||||
|
} else if changed {
|
||||||
|
data = forced
|
||||||
|
if err := statsStore.UpsertXrayConfig(context.Background(), m.configStoreKeyLocked(), forced); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
xrayLogf("xray: forced debug log output while syncing config")
|
||||||
|
}
|
||||||
pretty, err := normalizeJSONIndent(data)
|
pretty, err := normalizeJSONIndent(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1166,7 +1317,7 @@ func (m *XrayManager) SetConfig(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
log.Printf("xray: added/repaired Stats API support while saving config")
|
xrayLogf("xray: added/repaired Stats API support and forced debug logs while saving config")
|
||||||
}
|
}
|
||||||
return m.writeConfigLocked(patched)
|
return m.writeConfigLocked(patched)
|
||||||
}
|
}
|
||||||
@@ -1238,6 +1389,9 @@ func patchXrayStatsAPIBytes(data []byte) ([]byte, bool, error) {
|
|||||||
return nil, false, fmt.Errorf("xray config must be a JSON object")
|
return nil, false, fmt.Errorf("xray config must be a JSON object")
|
||||||
}
|
}
|
||||||
changed, _ := ensureXrayStatsAPIConfig(raw)
|
changed, _ := ensureXrayStatsAPIConfig(raw)
|
||||||
|
if ensureXrayForcedDebugLogConfig(raw) {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
if !changed {
|
if !changed {
|
||||||
return data, false, nil
|
return data, false, nil
|
||||||
}
|
}
|
||||||
@@ -1666,7 +1820,7 @@ func handleXrayStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
wasRunning := xrayMgr.isRunningSnapshot()
|
wasRunning := xrayMgr.isRunningSnapshot()
|
||||||
if changed, err := xrayMgr.EnsureStatsAPIConfig(); err == nil && changed && wasRunning {
|
if changed, err := xrayMgr.EnsureStatsAPIConfig(); err == nil && changed && wasRunning {
|
||||||
if err := xrayMgr.Restart(); err != nil {
|
if err := xrayMgr.Restart(); err != nil {
|
||||||
log.Printf("xray: auto stats repair restart failed: %v", err)
|
xrayLogf("xray: auto stats repair restart failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1992,7 +2146,7 @@ func (m *XrayManager) AddXrayClient(inboundTag, uuid, email string) error {
|
|||||||
})
|
})
|
||||||
if err == nil && m.cfg != nil && m.cfg.UseNative() {
|
if err == nil && m.cfg != nil && m.cfg.UseNative() {
|
||||||
if hotErr := nativeXray.addClient(inboundTag, uuid, email); hotErr != nil {
|
if hotErr := nativeXray.addClient(inboundTag, uuid, email); hotErr != nil {
|
||||||
log.Printf("native xray: hot-add client %s to %s failed: %v", uuid, inboundTag, hotErr)
|
xrayLogf("native xray: hot-add client %s to %s failed: %v", uuid, inboundTag, hotErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -2044,7 +2198,7 @@ func (m *XrayManager) RemoveXrayClient(inboundTag, uuid string) error {
|
|||||||
})
|
})
|
||||||
if err == nil && m.cfg != nil && m.cfg.UseNative() {
|
if err == nil && m.cfg != nil && m.cfg.UseNative() {
|
||||||
if hotErr := nativeXray.removeClient(inboundTag, uuid); hotErr != nil {
|
if hotErr := nativeXray.removeClient(inboundTag, uuid); hotErr != nil {
|
||||||
log.Printf("native xray: hot-remove client %s from %s failed: %v", uuid, inboundTag, hotErr)
|
xrayLogf("native xray: hot-remove client %s from %s failed: %v", uuid, inboundTag, hotErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -2090,7 +2244,7 @@ func (m *XrayManager) UpdateXrayClientEmail(uuid, email string) error {
|
|||||||
})
|
})
|
||||||
if err == nil && m.cfg != nil && m.cfg.UseNative() {
|
if err == nil && m.cfg != nil && m.cfg.UseNative() {
|
||||||
if hotErr := nativeXray.updateClientEmail(uuid, email); hotErr != nil {
|
if hotErr := nativeXray.updateClientEmail(uuid, email); hotErr != nil {
|
||||||
log.Printf("native xray: hot-update client %s email failed: %v", uuid, hotErr)
|
xrayLogf("native xray: hot-update client %s email failed: %v", uuid, hotErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -2359,7 +2513,7 @@ func handleXrayClientAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := statsStore.UpsertXrayClientMeta(r.Context(), meta); err != nil {
|
if err := statsStore.UpsertXrayClientMeta(r.Context(), meta); err != nil {
|
||||||
log.Printf("xray: save meta for %s: %v", req.UUID, err)
|
xrayLogf("xray: save meta for %s: %v", req.UUID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xrayMgr.restartIfExternalRunning()
|
xrayMgr.restartIfExternalRunning()
|
||||||
@@ -2445,7 +2599,7 @@ func handleXrayClientUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
if req.Email != "" {
|
if req.Email != "" {
|
||||||
if err := xrayMgr.UpdateXrayClientEmail(req.UUID, req.Email); err != nil {
|
if err := xrayMgr.UpdateXrayClientEmail(req.UUID, req.Email); err != nil {
|
||||||
log.Printf("xray: update config email for %s: %v", req.UUID, err)
|
xrayLogf("xray: update config email for %s: %v", req.UUID, err)
|
||||||
} else {
|
} else {
|
||||||
xrayMgr.restartIfExternalRunning()
|
xrayMgr.restartIfExternalRunning()
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-56
@@ -9,11 +9,12 @@ package main
|
|||||||
//
|
//
|
||||||
// Native emulator scope:
|
// Native emulator scope:
|
||||||
// - Protocols : VLESS and VMess AEAD (TCP + UDP commands)
|
// - Protocols : VLESS and VMess AEAD (TCP + UDP commands)
|
||||||
|
// - VLESS Mux : Mux.Cool child TCP/UDP sessions, including XUDP metadata
|
||||||
// - Transports: raw TCP, WebSocket (RFC 6455), XHTTP/SplitHTTP
|
// - Transports: raw TCP, WebSocket (RFC 6455), XHTTP/SplitHTTP
|
||||||
// - Security : TLS, none
|
// - Security : TLS, none
|
||||||
//
|
//
|
||||||
// Mux, REALITY, gRPC and HTTPUpgrade are still deferred; unsupported commands
|
// REALITY, gRPC and HTTPUpgrade are still deferred; unsupported commands are
|
||||||
// are rejected explicitly instead of silently falling back.
|
// rejected explicitly instead of silently falling back.
|
||||||
//
|
//
|
||||||
// Native mode has its own DB-backed config/runtime path. It does not spawn or
|
// Native mode has its own DB-backed config/runtime path. It does not spawn or
|
||||||
// query the external xray binary and does not require /opt/sshpanel/xray to be
|
// query the external xray binary and does not require /opt/sshpanel/xray to be
|
||||||
@@ -34,7 +35,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -160,13 +160,13 @@ func (s *nativeXrayServer) start(configFile string) error {
|
|||||||
serveLn = tls.NewListener(ln, ib.tlsConfig)
|
serveLn = tls.NewListener(ln, ib.tlsConfig)
|
||||||
}
|
}
|
||||||
opened = append(opened, serveLn)
|
opened = append(opened, serveLn)
|
||||||
go ib.serveXHTTPListener(serveLn)
|
xrayGo(fmt.Sprintf("native xray xhttp listener %s", addr), func() { ib.serveXHTTPListener(serveLn) })
|
||||||
} else {
|
} else {
|
||||||
opened = append(opened, serveLn)
|
opened = append(opened, serveLn)
|
||||||
go ib.acceptLoop(serveLn)
|
xrayGo(fmt.Sprintf("native xray accept loop %s", addr), func() { ib.acceptLoop(serveLn) })
|
||||||
}
|
}
|
||||||
active[ib.tag] = ib
|
active[ib.tag] = ib
|
||||||
log.Printf("native xray: serving %s/%s on %s (inbound %q, security=%s, %d clients)",
|
xrayLogf("native xray: serving %s/%s on %s (inbound %q, security=%s, %d clients)",
|
||||||
ib.protocol, ib.transport, addr, ib.tag, orNone(ib.security), ib.clientCount())
|
ib.protocol, ib.transport, addr, ib.tag, orNone(ib.security), ib.clientCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,25 +189,27 @@ func (s *nativeXrayServer) stop() {
|
|||||||
s.listeners = nil
|
s.listeners = nil
|
||||||
s.inboundsByTag = nil
|
s.inboundsByTag = nil
|
||||||
s.running = false
|
s.running = false
|
||||||
log.Printf("native xray: stopped")
|
xrayLogf("native xray: stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) acceptLoop(ln net.Listener) {
|
func (ib *nativeInbound) acceptLoop(ln net.Listener) {
|
||||||
|
defer xrayRecover(fmt.Sprintf("native xray accept loop inbound=%q", ib.tag))
|
||||||
for {
|
for {
|
||||||
c, err := ln.Accept()
|
c, err := ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isListenerClosed(err) {
|
if isListenerClosed(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("native xray: accept error on %s: %v", ln.Addr(), err)
|
xrayLogf("native xray: accept error on %s: %v", ln.Addr(), err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go ib.serve(c)
|
xrayGo(fmt.Sprintf("native xray connection remote=%s", c.RemoteAddr()), func() { ib.serve(c) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve terminates TLS + transport, then dispatches on protocol.
|
// serve terminates TLS + transport, then dispatches on protocol.
|
||||||
func (ib *nativeInbound) serve(raw net.Conn) {
|
func (ib *nativeInbound) serve(raw net.Conn) {
|
||||||
|
defer xrayRecover(fmt.Sprintf("native xray serve inbound=%q remote=%s", ib.tag, raw.RemoteAddr()))
|
||||||
defer raw.Close()
|
defer raw.Close()
|
||||||
|
|
||||||
if tc, ok := raw.(*net.TCPConn); ok {
|
if tc, ok := raw.(*net.TCPConn); ok {
|
||||||
@@ -222,7 +224,7 @@ func (ib *nativeInbound) serve(raw net.Conn) {
|
|||||||
tconn := tls.Server(raw, ib.tlsConfig)
|
tconn := tls.Server(raw, ib.tlsConfig)
|
||||||
_ = tconn.SetDeadline(time.Now().Add(tlsHandshakeTimeout))
|
_ = tconn.SetDeadline(time.Now().Add(tlsHandshakeTimeout))
|
||||||
if err := tconn.Handshake(); err != nil {
|
if err := tconn.Handshake(); err != nil {
|
||||||
log.Printf("native xray: tls handshake from %s failed: %v", raw.RemoteAddr(), err)
|
xrayLogf("native xray: tls handshake from %s failed: %v", raw.RemoteAddr(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = tconn.SetDeadline(time.Time{})
|
_ = tconn.SetDeadline(time.Time{})
|
||||||
@@ -237,15 +239,15 @@ func (ib *nativeInbound) serve(raw net.Conn) {
|
|||||||
case "ws", "websocket":
|
case "ws", "websocket":
|
||||||
ws, err := wsServerHandshake(conn, ib.path)
|
ws, err := wsServerHandshake(conn, ib.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("native xray: ws handshake from %s failed: %v", raw.RemoteAddr(), err)
|
xrayLogf("native xray: ws handshake from %s failed: %v", raw.RemoteAddr(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stream = ws
|
stream = ws
|
||||||
case "xhttp", "splithttp":
|
case "xhttp", "splithttp":
|
||||||
log.Printf("native xray: inbound %q got raw connection for XHTTP; this transport is served by http.Server", ib.tag)
|
xrayLogf("native xray: inbound %q got raw connection for XHTTP; this transport is served by http.Server", ib.tag)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
log.Printf("native xray: inbound %q transport %q not supported yet; dropping conn from %s",
|
xrayLogf("native xray: inbound %q transport %q not supported yet; dropping conn from %s",
|
||||||
ib.tag, ib.transport, raw.RemoteAddr())
|
ib.tag, ib.transport, raw.RemoteAddr())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -257,7 +259,7 @@ func (ib *nativeInbound) serve(raw net.Conn) {
|
|||||||
case "vmess":
|
case "vmess":
|
||||||
ib.handleVMess(stream, raw.RemoteAddr())
|
ib.handleVMess(stream, raw.RemoteAddr())
|
||||||
default:
|
default:
|
||||||
log.Printf("native xray: inbound %q protocol %q not supported yet; dropping conn from %s",
|
xrayLogf("native xray: inbound %q protocol %q not supported yet; dropping conn from %s",
|
||||||
ib.tag, ib.protocol, raw.RemoteAddr())
|
ib.tag, ib.protocol, raw.RemoteAddr())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,9 +272,12 @@ func (ib *nativeInbound) serve(raw net.Conn) {
|
|||||||
// 1 byte addon length M
|
// 1 byte addon length M
|
||||||
// M bytes addons (flow etc.) — skipped
|
// M bytes addons (flow etc.) — skipped
|
||||||
// 1 byte command (1=TCP, 2=UDP, 3=Mux)
|
// 1 byte command (1=TCP, 2=UDP, 3=Mux)
|
||||||
// 2 bytes port (big endian)
|
// for TCP/UDP only:
|
||||||
// 1 byte address type (1=IPv4, 2=domain, 3=IPv6)
|
// 2 bytes port (big endian)
|
||||||
// ... address
|
// 1 byte address type (1=IPv4, 2=domain, 3=IPv6)
|
||||||
|
// ... address
|
||||||
|
// for Mux:
|
||||||
|
// ... Mux.Cool/XUDP frames immediately after the command byte
|
||||||
// ... payload
|
// ... payload
|
||||||
// Response (server -> client): 1 byte version echo, 1 byte addon length (0).
|
// Response (server -> client): 1 byte version echo, 1 byte addon length (0).
|
||||||
|
|
||||||
@@ -287,12 +292,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (ib *nativeInbound) handleVLESS(stream net.Conn, remote net.Addr) {
|
func (ib *nativeInbound) handleVLESS(stream net.Conn, remote net.Addr) {
|
||||||
log.Printf("native xray: vless handshake start inbound=%q transport=%s remote=%s", ib.tag, ib.transport, remote)
|
defer xrayRecover(fmt.Sprintf("native xray VLESS inbound=%q remote=%s", ib.tag, remote))
|
||||||
|
if ib.isXHTTP() {
|
||||||
|
xrayTracef("native xray: vless handshake start inbound=%q transport=%s remote=%s", ib.tag, ib.transport, remote)
|
||||||
|
} else {
|
||||||
|
xrayLogf("native xray: vless handshake start inbound=%q transport=%s remote=%s", ib.tag, ib.transport, remote)
|
||||||
|
}
|
||||||
_ = stream.SetReadDeadline(time.Now().Add(30 * time.Second))
|
_ = stream.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
|
||||||
head := make([]byte, 1+16+1) // version + uuid + addonLen
|
head := make([]byte, 1+16+1) // version + uuid + addonLen
|
||||||
if _, err := io.ReadFull(stream, head); err != nil {
|
if _, err := io.ReadFull(stream, head); err != nil {
|
||||||
log.Printf("native xray: vless handshake failed inbound=%q transport=%s remote=%s: %v", ib.tag, ib.transport, remote, err)
|
ib.logVLESSReadFailure("handshake", remote, "", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
version := head[0]
|
version := head[0]
|
||||||
@@ -301,39 +311,59 @@ func (ib *nativeInbound) handleVLESS(stream net.Conn, remote net.Addr) {
|
|||||||
|
|
||||||
client := ib.getNativeClient(id)
|
client := ib.getNativeClient(id)
|
||||||
if client == nil {
|
if client == nil {
|
||||||
log.Printf("native xray: inbound %q rejected unknown VLESS uuid from %s", ib.tag, remote)
|
xrayLogf("native xray: inbound %q rejected unknown VLESS uuid from %s", ib.tag, remote)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if addonLen := int(head[17]); addonLen > 0 {
|
if addonLen := int(head[17]); addonLen > 0 {
|
||||||
if _, err := io.CopyN(io.Discard, stream, int64(addonLen)); err != nil {
|
if _, err := io.CopyN(io.Discard, stream, int64(addonLen)); err != nil {
|
||||||
log.Printf("native xray: vless addon read failed inbound=%q user=%s: %v", ib.tag, client.email, err)
|
xrayLogf("native xray: vless addon read failed inbound=%q user=%s: %v", ib.tag, client.email, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd [1]byte
|
var cmd [1]byte
|
||||||
if _, err := io.ReadFull(stream, cmd[:]); err != nil {
|
if _, err := io.ReadFull(stream, cmd[:]); err != nil {
|
||||||
log.Printf("native xray: vless command read failed inbound=%q user=%s: %v", ib.tag, client.email, err)
|
ib.logVLESSReadFailure("command", remote, client.email, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var portBuf [2]byte
|
|
||||||
if _, err := io.ReadFull(stream, portBuf[:]); err != nil {
|
|
||||||
log.Printf("native xray: vless port read failed inbound=%q user=%s: %v", ib.tag, client.email, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
port := binary.BigEndian.Uint16(portBuf[:])
|
|
||||||
|
|
||||||
host, err := readProxyAddress(stream)
|
var host string
|
||||||
if err != nil {
|
var port uint16
|
||||||
log.Printf("native xray: inbound %q VLESS bad address from %s: %v", ib.tag, remote, err)
|
if cmd[0] == vlessCmdTCP || cmd[0] == vlessCmdUDP {
|
||||||
return
|
var portBuf [2]byte
|
||||||
|
if _, err := io.ReadFull(stream, portBuf[:]); err != nil {
|
||||||
|
xrayLogf("native xray: vless port read failed inbound=%q user=%s: %v", ib.tag, client.email, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
port = binary.BigEndian.Uint16(portBuf[:])
|
||||||
|
|
||||||
|
var err error
|
||||||
|
host, err = readProxyAddress(stream)
|
||||||
|
if err != nil {
|
||||||
|
xrayLogf("native xray: inbound %q VLESS bad address from %s: %v", ib.tag, remote, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isNativeDNSSinkTarget(host) {
|
||||||
|
_ = stream.SetReadDeadline(time.Time{})
|
||||||
|
_ = stream.SetWriteDeadline(time.Now().Add(time.Second))
|
||||||
|
_, _ = stream.Write([]byte{version, 0})
|
||||||
|
xrayTracef("native xray: inbound %q fast-ignored DNS sink target cmd=%d user=%s host=%q port=%d remote=%s", ib.tag, cmd[0], client.email, host, port, remote)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if invalidNativeDestination(host, port) {
|
||||||
|
xrayTracef("native xray: inbound %q rejected invalid VLESS target cmd=%d user=%s host=%q port=%d remote=%s", ib.tag, cmd[0], client.email, host, port, remote)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = stream.SetReadDeadline(time.Time{})
|
_ = stream.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
// VLESS response header must be sent before relaying payload.
|
// VLESS response header must be sent before relaying payload. CommandMux is
|
||||||
|
// special: official Xray does not read a target from the VLESS header for it;
|
||||||
|
// the following bytes are Mux.Cool/XUDP frames. Reading port/address here
|
||||||
|
// deadlocks muxed UDP clients and shows up as QUIC/YouTube stalls.
|
||||||
if _, err := stream.Write([]byte{version, 0}); err != nil {
|
if _, err := stream.Write([]byte{version, 0}); err != nil {
|
||||||
log.Printf("native xray: vless response write failed inbound=%q user=%s: %v", ib.tag, client.email, err)
|
xrayLogf("native xray: vless response write failed inbound=%q user=%s: %v", ib.tag, client.email, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,24 +371,79 @@ func (ib *nativeInbound) handleVLESS(stream net.Conn, remote net.Addr) {
|
|||||||
case vlessCmdTCP:
|
case vlessCmdTCP:
|
||||||
backend, target, err := ib.nativeDialTCP(host, port)
|
backend, target, err := ib.nativeDialTCP(host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("native xray: inbound %q VLESS TCP dial %s failed: %v", ib.tag, target, err)
|
xrayLogf("native xray: inbound %q VLESS TCP dial %s failed: %v", ib.tag, target, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("native xray: vless/tcp user=%s src=%s -> %s (inbound %q)", client.email, backend.LocalAddr(), target, ib.tag)
|
ib.nativeSuccessLogf("native xray: vless/tcp user=%s src=%s -> %s (inbound %q)", client.email, backend.LocalAddr(), target, ib.tag)
|
||||||
nativeTunnel(stream, backend, client.uuid, client.email, ib.upLimiter(), ib.downLimiter())
|
nativeTunnel(stream, backend, client.uuid, client.email, ib.upLimiter(), ib.downLimiter())
|
||||||
case vlessCmdUDP:
|
case vlessCmdUDP:
|
||||||
backend, target, err := ib.nativeDialUDP(host, port)
|
backend, target, err := ib.nativeDialUDP(host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("native xray: inbound %q VLESS UDP dial %s failed: %v", ib.tag, target, err)
|
xrayLogf("native xray: inbound %q VLESS UDP dial %s failed: %v", ib.tag, target, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("native xray: vless/udp user=%s src=%s -> %s (inbound %q)", client.email, backend.LocalAddr(), target, ib.tag)
|
ib.nativeSuccessLogf("native xray: vless/udp user=%s src=%s -> %s (inbound %q)", client.email, backend.LocalAddr(), target, ib.tag)
|
||||||
nativeVLESSUDPTunnel(stream, backend, client.uuid, client.email, ib.upLimiter(), ib.downLimiter())
|
nativeVLESSUDPTunnel(stream, backend, client.uuid, client.email, ib.upLimiter(), ib.downLimiter())
|
||||||
|
case vlessCmdMux:
|
||||||
|
ib.nativeSuccessLogf("native xray: vless/mux user=%s remote=%s (inbound %q)", client.email, remote, ib.tag)
|
||||||
|
ib.nativeVLESSMuxTunnel(stream, client.uuid, client.email)
|
||||||
default:
|
default:
|
||||||
log.Printf("native xray: inbound %q VLESS command %d not supported yet", ib.tag, cmd[0])
|
xrayLogf("native xray: inbound %q VLESS command %d not supported yet", ib.tag, cmd[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ib *nativeInbound) logVLESSReadFailure(stage string, remote net.Addr, email string, err error) {
|
||||||
|
if ib.isXHTTP() && isNativeDeadlineError(err) {
|
||||||
|
if email == "" {
|
||||||
|
xrayTracef("native xray: vless %s timed out inbound=%q transport=%s remote=%s: %v", stage, ib.tag, ib.transport, remote, err)
|
||||||
|
} else {
|
||||||
|
xrayTracef("native xray: vless %s timed out inbound=%q transport=%s user=%s remote=%s: %v", stage, ib.tag, ib.transport, email, remote, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if email == "" {
|
||||||
|
xrayLogf("native xray: vless %s failed inbound=%q transport=%s remote=%s: %v", stage, ib.tag, ib.transport, remote, err)
|
||||||
|
} else {
|
||||||
|
xrayLogf("native xray: vless %s failed inbound=%q transport=%s user=%s remote=%s: %v", stage, ib.tag, ib.transport, email, remote, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNativeDeadlineError(err error) bool {
|
||||||
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var ne net.Error
|
||||||
|
return errors.As(err, &ne) && ne.Timeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidNativeDestination(host string, port uint16) bool {
|
||||||
|
host = strings.TrimSpace(normalizeNativeTargetHost(host))
|
||||||
|
if host == "" || port == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(stripNativeIPZone(host)); ip != nil {
|
||||||
|
return ip.IsUnspecified()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNativeDNSSinkTarget(host string) bool {
|
||||||
|
host = strings.TrimSpace(normalizeNativeTargetHost(host))
|
||||||
|
if host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(stripNativeIPZone(host))
|
||||||
|
return ip != nil && ip.IsUnspecified()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ib *nativeInbound) nativeSuccessLogf(format string, args ...interface{}) {
|
||||||
|
if ib != nil && ib.isXHTTP() {
|
||||||
|
xrayTracef(format, args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xrayLogf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
// readProxyAddress reads a VMess/VLESS-style address (type byte + address).
|
// readProxyAddress reads a VMess/VLESS-style address (type byte + address).
|
||||||
func readProxyAddress(r io.Reader) (string, error) {
|
func readProxyAddress(r io.Reader) (string, error) {
|
||||||
var t [1]byte
|
var t [1]byte
|
||||||
@@ -445,13 +530,13 @@ func nativeDialTargetWithSource(network, host string, port uint16, sourceHost st
|
|||||||
cancel()
|
cancel()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if i > 0 && len(attempts) > 1 {
|
if i > 0 && len(attempts) > 1 {
|
||||||
log.Printf("native xray: outbound dial recovered target=%s network=%s using auto source after bound source failed", target, dialNetwork)
|
xrayLogf("native xray: outbound dial recovered target=%s network=%s using auto source after bound source failed", target, dialNetwork)
|
||||||
}
|
}
|
||||||
return conn, target, nil
|
return conn, target, nil
|
||||||
}
|
}
|
||||||
lastErr = err
|
lastErr = err
|
||||||
if local != nil {
|
if local != nil {
|
||||||
log.Printf("native xray: outbound dial target=%s network=%s source=%s failed, retrying auto source: %v", target, dialNetwork, local.String(), err)
|
xrayLogf("native xray: outbound dial target=%s network=%s source=%s failed, retrying auto source: %v", target, dialNetwork, local.String(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, target, lastErr
|
return nil, target, lastErr
|
||||||
@@ -537,31 +622,36 @@ func normalizeNativeTargetHost(raw string) string {
|
|||||||
func nativeTunnel(client io.ReadWriteCloser, backend net.Conn, uuid, email string, up, down *rate.Limiter) {
|
func nativeTunnel(client io.ReadWriteCloser, backend net.Conn, uuid, email string, up, down *rate.Limiter) {
|
||||||
xrayMgr.recordNativeConnect(uuid, email)
|
xrayMgr.recordNativeConnect(uuid, email)
|
||||||
defer xrayMgr.recordNativeDisconnect(uuid, email)
|
defer xrayMgr.recordNativeDisconnect(uuid, email)
|
||||||
|
defer xrayRecover(fmt.Sprintf("native xray TCP tunnel user=%s", email))
|
||||||
|
|
||||||
upMeter := &trafficMeter{uuid: uuid, email: email, uplink: true}
|
upMeter := &trafficMeter{uuid: uuid, email: email, uplink: true}
|
||||||
downMeter := &trafficMeter{uuid: uuid, email: email, uplink: false}
|
downMeter := &trafficMeter{uuid: uuid, email: email, uplink: false}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
var closeOnce sync.Once
|
||||||
closeAll := func() {
|
closeAll := func() {
|
||||||
_ = backend.Close()
|
closeOnce.Do(func() {
|
||||||
_ = client.Close()
|
_ = backend.Close()
|
||||||
|
_ = client.Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() { // client -> backend (uplink)
|
xrayGo("native xray TCP uplink", func() { // client -> backend
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer closeAll()
|
||||||
_, _ = copyWithRateLimit(meteredWriter{w: backend, meter: upMeter}, client, up)
|
_, _ = copyWithRateLimit(meteredWriter{w: backend, meter: upMeter}, client, up)
|
||||||
if cw, ok := backend.(interface{ CloseWrite() error }); ok {
|
if cw, ok := backend.(interface{ CloseWrite() error }); ok {
|
||||||
_ = cw.CloseWrite()
|
_ = cw.CloseWrite()
|
||||||
}
|
}
|
||||||
closeAll()
|
})
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() { // backend -> client (downlink)
|
xrayGo("native xray TCP downlink", func() { // backend -> client
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer closeAll()
|
||||||
_, _ = copyWithRateLimit(meteredWriter{w: client, meter: downMeter}, backend, down)
|
_, _ = copyWithRateLimit(meteredWriter{w: client, meter: downMeter}, backend, down)
|
||||||
closeAll()
|
})
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
upMeter.flush()
|
upMeter.flush()
|
||||||
@@ -853,7 +943,7 @@ func parseNativeInbounds(configFile string) ([]*nativeInbound, error) {
|
|||||||
}
|
}
|
||||||
port, ok := parseSinglePort(in.Port)
|
port, ok := parseSinglePort(in.Port)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("native xray: inbound %q has unsupported port form; skipping", in.Tag)
|
xrayLogf("native xray: inbound %q has unsupported port form; skipping", in.Tag)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,7 +984,7 @@ func parseNativeInbounds(configFile string) ([]*nativeInbound, error) {
|
|||||||
}
|
}
|
||||||
ib.xhttpMaxBufferedPosts = xh.ScMaxBufferedPosts
|
ib.xhttpMaxBufferedPosts = xh.ScMaxBufferedPosts
|
||||||
if ib.xhttpMaxBufferedPosts <= 0 {
|
if ib.xhttpMaxBufferedPosts <= 0 {
|
||||||
ib.xhttpMaxBufferedPosts = 30
|
ib.xhttpMaxBufferedPosts = nativeXHTTPBufferedPostLimit()
|
||||||
}
|
}
|
||||||
ib.xhttpSessions = make(map[string]*nativeXHTTPSession)
|
ib.xhttpSessions = make(map[string]*nativeXHTTPSession)
|
||||||
}
|
}
|
||||||
@@ -904,7 +994,7 @@ func parseNativeInbounds(configFile string) ([]*nativeInbound, error) {
|
|||||||
if ib.security == "tls" {
|
if ib.security == "tls" {
|
||||||
tc, err := buildInboundTLS(in)
|
tc, err := buildInboundTLS(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("native xray: inbound %q TLS disabled: %v; skipping", in.Tag, err)
|
xrayLogf("native xray: inbound %q TLS disabled: %v; skipping", in.Tag, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ib.tlsConfig = tc
|
ib.tlsConfig = tc
|
||||||
@@ -920,24 +1010,24 @@ func parseNativeInbounds(configFile string) ([]*nativeInbound, error) {
|
|||||||
raw = c.Password // some protocols reuse password as id
|
raw = c.Password // some protocols reuse password as id
|
||||||
}
|
}
|
||||||
if err := ib.addNativeClient(proto, raw, c.Email); err != nil {
|
if err := ib.addNativeClient(proto, raw, c.Email); err != nil {
|
||||||
log.Printf("native xray: inbound %q skipping client %q: %v", in.Tag, raw, err)
|
xrayLogf("native xray: inbound %q skipping client %q: %v", in.Tag, raw, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if statsStore != nil && in.Tag != "" {
|
if statsStore != nil && in.Tag != "" {
|
||||||
metas, err := statsStore.ListXrayClientsByInbound(context.Background(), in.Tag)
|
metas, err := statsStore.ListXrayClientsByInbound(context.Background(), in.Tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("native xray: inbound %q database clients unavailable: %v", in.Tag, err)
|
xrayLogf("native xray: inbound %q database clients unavailable: %v", in.Tag, err)
|
||||||
} else {
|
} else {
|
||||||
for _, m := range metas {
|
for _, m := range metas {
|
||||||
if err := ib.addNativeClient(proto, m.UUID, firstNonEmpty(m.Email, m.Name, m.UUID)); err != nil {
|
if err := ib.addNativeClient(proto, m.UUID, firstNonEmpty(m.Email, m.Name, m.UUID)); err != nil {
|
||||||
log.Printf("native xray: inbound %q skipping DB client %q: %v", in.Tag, m.UUID, err)
|
xrayLogf("native xray: inbound %q skipping DB client %q: %v", in.Tag, m.UUID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ib.clientCount() == 0 {
|
if ib.clientCount() == 0 {
|
||||||
log.Printf("native xray: inbound %q has no valid clients; skipping", in.Tag)
|
xrayLogf("native xray: inbound %q has no valid clients; skipping", in.Tag)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, ib)
|
out = append(out, ib)
|
||||||
|
|||||||
@@ -115,6 +115,66 @@ func TestVLESSOverTCP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func vlessDNSSinkHeader(id [16]byte, cmd byte, host net.IP, port uint16) []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.WriteByte(0)
|
||||||
|
b.Write(id[:])
|
||||||
|
b.WriteByte(0)
|
||||||
|
b.WriteByte(cmd)
|
||||||
|
b.WriteByte(byte(port >> 8))
|
||||||
|
b.WriteByte(byte(port))
|
||||||
|
if ip4 := host.To4(); ip4 != nil {
|
||||||
|
b.WriteByte(atypIPv4)
|
||||||
|
b.Write(ip4)
|
||||||
|
} else {
|
||||||
|
b.WriteByte(atypIPv6)
|
||||||
|
b.Write(host.To16())
|
||||||
|
}
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVLESSDNSSinkTargetFastIgnored(t *testing.T) {
|
||||||
|
_, port, id, stop := newTestInbound(t, "tcp", "")
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial inbound: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
conn.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
|
||||||
|
if _, err := conn.Write(vlessDNSSinkHeader(id, vlessCmdTCP, net.IPv4(0, 0, 0, 0), 0)); err != nil {
|
||||||
|
t.Fatalf("write sink header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||||
|
t.Fatalf("read VLESS sink response: %v", err)
|
||||||
|
}
|
||||||
|
if resp[0] != 0 || resp[1] != 0 {
|
||||||
|
t.Fatalf("bad VLESS sink response: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
if _, err := conn.Read(buf); err == nil {
|
||||||
|
t.Fatalf("sink target should close immediately after response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNativeDNSSinkTargetDetection(t *testing.T) {
|
||||||
|
for _, host := range []string{"0.0.0.0", "::", "[::]", "::%lo"} {
|
||||||
|
if !isNativeDNSSinkTarget(host) {
|
||||||
|
t.Fatalf("%q should be detected as DNS sink", host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, host := range []string{"", "127.0.0.1", "1.1.1.1", "example.com"} {
|
||||||
|
if isNativeDNSSinkTarget(host) {
|
||||||
|
t.Fatalf("%q should not be detected as DNS sink", host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVLESSRejectsUnknownUUID(t *testing.T) {
|
func TestVLESSRejectsUnknownUUID(t *testing.T) {
|
||||||
echoPort, stopEcho := startEchoServer(t)
|
echoPort, stopEcho := startEchoServer(t)
|
||||||
defer stopEcho()
|
defer stopEcho()
|
||||||
@@ -578,3 +638,243 @@ func TestNativeLocalAddrForIPv6Tunnel(t *testing.T) {
|
|||||||
t.Fatalf("must not bind IPv4 source to IPv6 target: %#v", local)
|
t.Fatalf("must not bind IPv4 source to IPv6 target: %#v", local)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startUDPEchoServer(t *testing.T) (int, func()) {
|
||||||
|
t.Helper()
|
||||||
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("udp echo listen: %v", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 64*1024)
|
||||||
|
for {
|
||||||
|
n, addr, err := pc.ReadFrom(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = pc.WriteTo(buf[:n], addr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return pc.LocalAddr().(*net.UDPAddr).Port, func() { pc.Close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func vlessMuxHeader(id [16]byte) []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.WriteByte(0)
|
||||||
|
b.Write(id[:])
|
||||||
|
b.WriteByte(0)
|
||||||
|
b.WriteByte(vlessCmdMux)
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMuxUDPFrame(sessionID uint16, host string, port int, payload []byte) []byte {
|
||||||
|
meta := []byte{byte(sessionID >> 8), byte(sessionID), nativeMuxStatusNew, nativeMuxOptionData, nativeMuxNetworkUDP}
|
||||||
|
meta = appendNativeMuxAddressPort(meta, host, uint16(port))
|
||||||
|
var out bytes.Buffer
|
||||||
|
binary.Write(&out, binary.BigEndian, uint16(len(meta)))
|
||||||
|
out.Write(meta)
|
||||||
|
binary.Write(&out, binary.BigEndian, uint16(len(payload)))
|
||||||
|
out.Write(payload)
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMuxTCPFrame(sessionID uint16, host string, port int, payload []byte) []byte {
|
||||||
|
meta := []byte{byte(sessionID >> 8), byte(sessionID), nativeMuxStatusNew, nativeMuxOptionData, nativeMuxNetworkTCP}
|
||||||
|
meta = appendNativeMuxAddressPort(meta, host, uint16(port))
|
||||||
|
var out bytes.Buffer
|
||||||
|
binary.Write(&out, binary.BigEndian, uint16(len(meta)))
|
||||||
|
out.Write(meta)
|
||||||
|
binary.Write(&out, binary.BigEndian, uint16(len(payload)))
|
||||||
|
out.Write(payload)
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMuxXUDPFrame(sessionID uint16, host string, port int, payload []byte, gid [8]byte) []byte {
|
||||||
|
meta := []byte{byte(sessionID >> 8), byte(sessionID), nativeMuxStatusNew, nativeMuxOptionData, nativeMuxNetworkUDP}
|
||||||
|
meta = appendNativeMuxAddressPort(meta, host, uint16(port))
|
||||||
|
meta = append(meta, gid[:]...)
|
||||||
|
var out bytes.Buffer
|
||||||
|
binary.Write(&out, binary.BigEndian, uint16(len(meta)))
|
||||||
|
out.Write(meta)
|
||||||
|
// Official Mux.Cool/XUDP carries GlobalID in the outer New metadata. The
|
||||||
|
// following packet block is the UDP datagram itself, not another XUDP metadata
|
||||||
|
// stream.
|
||||||
|
binary.Write(&out, binary.BigEndian, uint16(len(payload)))
|
||||||
|
out.Write(payload)
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVLESSMuxUDPDoesNotStall(t *testing.T) {
|
||||||
|
udpPort, stopUDP := startUDPEchoServer(t)
|
||||||
|
defer stopUDP()
|
||||||
|
_, port, id, stop := newTestInbound(t, "tcp", "")
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial inbound: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
|
||||||
|
if _, err := conn.Write(vlessMuxHeader(id)); err != nil {
|
||||||
|
t.Fatalf("write mux header: %v", err)
|
||||||
|
}
|
||||||
|
resp := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||||
|
t.Fatalf("read mux response header: %v", err)
|
||||||
|
}
|
||||||
|
if resp[0] != 0 || resp[1] != 0 {
|
||||||
|
t.Fatalf("bad mux vless response: %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte("quic-over-mux")
|
||||||
|
if _, err := conn.Write(buildMuxUDPFrame(7, "127.0.0.1", udpPort, want)); err != nil {
|
||||||
|
t.Fatalf("write mux udp frame: %v", err)
|
||||||
|
}
|
||||||
|
meta, err := readNativeMuxMetadata(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read mux response meta: %v", err)
|
||||||
|
}
|
||||||
|
if meta.sessionID != 7 || meta.status != nativeMuxStatusKeep || meta.option&nativeMuxOptionData == 0 {
|
||||||
|
t.Fatalf("bad mux response metadata: %#v", meta)
|
||||||
|
}
|
||||||
|
got, err := readNativeMuxDataBlock(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read mux response payload: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("mux udp echo mismatch: got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVLESSMuxXUDPDoesNotStall(t *testing.T) {
|
||||||
|
udpPort, stopUDP := startUDPEchoServer(t)
|
||||||
|
defer stopUDP()
|
||||||
|
_, port, id, stop := newTestInbound(t, "tcp", "")
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial inbound: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
|
||||||
|
if _, err := conn.Write(vlessMuxHeader(id)); err != nil {
|
||||||
|
t.Fatalf("write mux header: %v", err)
|
||||||
|
}
|
||||||
|
resp := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||||
|
t.Fatalf("read mux response header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte("quic-over-xudp")
|
||||||
|
gid := [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
|
||||||
|
if _, err := conn.Write(buildMuxXUDPFrame(9, "127.0.0.1", udpPort, want, gid)); err != nil {
|
||||||
|
t.Fatalf("write mux xudp frame: %v", err)
|
||||||
|
}
|
||||||
|
meta, err := readNativeMuxMetadata(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read mux xudp response meta: %v", err)
|
||||||
|
}
|
||||||
|
if meta.sessionID != 9 || meta.status != nativeMuxStatusKeep || meta.option&nativeMuxOptionData == 0 {
|
||||||
|
t.Fatalf("bad mux xudp response metadata: %#v", meta)
|
||||||
|
}
|
||||||
|
if meta.host != "127.0.0.1" || int(meta.port) != udpPort {
|
||||||
|
t.Fatalf("xudp response did not preserve UDP endpoint: %#v", meta)
|
||||||
|
}
|
||||||
|
got, err := readNativeMuxDataBlock(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read xudp response payload: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("mux xudp echo mismatch: got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVLESSMuxXUDPPayloadLookingLikeMetadataDoesNotStall(t *testing.T) {
|
||||||
|
udpPort, stopUDP := startUDPEchoServer(t)
|
||||||
|
defer stopUDP()
|
||||||
|
_, port, id, stop := newTestInbound(t, "tcp", "")
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial inbound: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
|
||||||
|
if _, err := conn.Write(vlessMuxHeader(id)); err != nil {
|
||||||
|
t.Fatalf("write mux header: %v", err)
|
||||||
|
}
|
||||||
|
resp := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||||
|
t.Fatalf("read mux response header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This payload intentionally looks like old inner-XUDP metadata. Mux.Cool
|
||||||
|
// packet mode must still forward it as one UDP datagram and must not block
|
||||||
|
// waiting for another fake payload block.
|
||||||
|
want := []byte{0, 0, 2, 1, 'q', 'u', 'i', 'c'}
|
||||||
|
gid := [8]byte{8, 7, 6, 5, 4, 3, 2, 1}
|
||||||
|
if _, err := conn.Write(buildMuxXUDPFrame(10, "127.0.0.1", udpPort, want, gid)); err != nil {
|
||||||
|
t.Fatalf("write mux xudp frame: %v", err)
|
||||||
|
}
|
||||||
|
meta, err := readNativeMuxMetadata(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read mux xudp response meta: %v", err)
|
||||||
|
}
|
||||||
|
if meta.sessionID != 10 || meta.status != nativeMuxStatusKeep || meta.option&nativeMuxOptionData == 0 {
|
||||||
|
t.Fatalf("bad mux xudp response metadata: %#v", meta)
|
||||||
|
}
|
||||||
|
got, err := readNativeMuxDataBlock(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read xudp response payload: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("mux xudp metadata-looking payload changed: got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVLESSMuxTCPDoesNotStall(t *testing.T) {
|
||||||
|
tcpPort, stopTCP := startEchoServer(t)
|
||||||
|
defer stopTCP()
|
||||||
|
_, port, id, stop := newTestInbound(t, "tcp", "")
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial inbound: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
|
||||||
|
if _, err := conn.Write(vlessMuxHeader(id)); err != nil {
|
||||||
|
t.Fatalf("write mux header: %v", err)
|
||||||
|
}
|
||||||
|
resp := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||||
|
t.Fatalf("read mux response header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []byte("tcp-over-mux")
|
||||||
|
if _, err := conn.Write(buildMuxTCPFrame(11, "127.0.0.1", tcpPort, want)); err != nil {
|
||||||
|
t.Fatalf("write mux tcp frame: %v", err)
|
||||||
|
}
|
||||||
|
meta, err := readNativeMuxMetadata(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read mux tcp response meta: %v", err)
|
||||||
|
}
|
||||||
|
if meta.sessionID != 11 || meta.status != nativeMuxStatusKeep || meta.option&nativeMuxOptionData == 0 {
|
||||||
|
t.Fatalf("bad mux tcp response metadata: %#v", meta)
|
||||||
|
}
|
||||||
|
got, err := readNativeMuxDataBlock(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read mux tcp response payload: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("mux tcp echo mismatch: got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+38
-27
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -29,24 +28,29 @@ const (
|
|||||||
func nativeVLESSUDPTunnel(client io.ReadWriteCloser, backend net.Conn, uuid, email string, up, down *rate.Limiter) {
|
func nativeVLESSUDPTunnel(client io.ReadWriteCloser, backend net.Conn, uuid, email string, up, down *rate.Limiter) {
|
||||||
xrayMgr.recordNativeConnect(uuid, email)
|
xrayMgr.recordNativeConnect(uuid, email)
|
||||||
defer xrayMgr.recordNativeDisconnect(uuid, email)
|
defer xrayMgr.recordNativeDisconnect(uuid, email)
|
||||||
|
defer xrayRecover(fmt.Sprintf("native xray VLESS UDP tunnel user=%s", email))
|
||||||
|
|
||||||
upMeter := &trafficMeter{uuid: uuid, email: email, uplink: true}
|
upMeter := &trafficMeter{uuid: uuid, email: email, uplink: true}
|
||||||
downMeter := &trafficMeter{uuid: uuid, email: email, uplink: false}
|
downMeter := &trafficMeter{uuid: uuid, email: email, uplink: false}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
var closeOnce sync.Once
|
||||||
closeAll := func() {
|
closeAll := func() {
|
||||||
_ = backend.Close()
|
closeOnce.Do(func() {
|
||||||
_ = client.Close()
|
_ = backend.Close()
|
||||||
|
_ = client.Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
xrayGo("native xray VLESS UDP uplink", func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer closeAll()
|
||||||
for {
|
for {
|
||||||
payload, err := readVLESSLengthPacket(client)
|
payload, err := readVLESSLengthPacket(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Printf("native xray: VLESS UDP client read failed: %v", err)
|
xrayLogf("native xray: VLESS UDP client read failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -61,15 +65,16 @@ func nativeVLESSUDPTunnel(client io.ReadWriteCloser, backend net.Conn, uuid, ema
|
|||||||
upMeter.add(n)
|
upMeter.add(n)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("native xray: VLESS UDP backend write failed: %v", err)
|
xrayLogf("native xray: VLESS UDP backend write failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
xrayGo("native xray VLESS UDP downlink", func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer closeAll()
|
||||||
buf := make([]byte, nativeUDPBufferSize)
|
buf := make([]byte, nativeUDPBufferSize)
|
||||||
for {
|
for {
|
||||||
_ = backend.SetReadDeadline(time.Now().Add(nativeUDPIdle))
|
_ = backend.SetReadDeadline(time.Now().Add(nativeUDPIdle))
|
||||||
@@ -79,7 +84,7 @@ func nativeVLESSUDPTunnel(client io.ReadWriteCloser, backend net.Conn, uuid, ema
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Printf("native xray: VLESS UDP backend read failed: %v", err)
|
xrayLogf("native xray: VLESS UDP backend read failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -90,12 +95,12 @@ func nativeVLESSUDPTunnel(client io.ReadWriteCloser, backend net.Conn, uuid, ema
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := writeVLESSLengthPacket(client, buf[:n]); err != nil {
|
if err := writeVLESSLengthPacket(client, buf[:n]); err != nil {
|
||||||
log.Printf("native xray: VLESS UDP client write failed: %v", err)
|
xrayLogf("native xray: VLESS UDP client write failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
downMeter.add(n)
|
downMeter.add(n)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
upMeter.flush()
|
upMeter.flush()
|
||||||
@@ -198,12 +203,12 @@ func writeVLESSLengthPacket(w io.Writer, payload []byte) error {
|
|||||||
if len(payload) > nativeUDPMaxPacket {
|
if len(payload) > nativeUDPMaxPacket {
|
||||||
return fmt.Errorf("udp packet too large: %d", len(payload))
|
return fmt.Errorf("udp packet too large: %d", len(payload))
|
||||||
}
|
}
|
||||||
var lenBuf [2]byte
|
// One Write is important for XHTTP because the response writer flushes once per
|
||||||
binary.BigEndian.PutUint16(lenBuf[:], uint16(len(payload)))
|
// Write. Two writes per UDP packet doubles flush/syscall pressure.
|
||||||
if _, err := w.Write(lenBuf[:]); err != nil {
|
frame := make([]byte, 0, 2+len(payload))
|
||||||
return err
|
frame = appendUint16(frame, uint16(len(payload)))
|
||||||
}
|
frame = append(frame, payload...)
|
||||||
_, err := w.Write(payload)
|
_, err := w.Write(frame)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,24 +304,29 @@ func writeVLESSXUDPPacket(w io.Writer, payload []byte) error {
|
|||||||
func nativeVMessUDPTunnel(client nativeVMessStream, backend net.Conn, uuid, email string, up, down *rate.Limiter) {
|
func nativeVMessUDPTunnel(client nativeVMessStream, backend net.Conn, uuid, email string, up, down *rate.Limiter) {
|
||||||
xrayMgr.recordNativeConnect(uuid, email)
|
xrayMgr.recordNativeConnect(uuid, email)
|
||||||
defer xrayMgr.recordNativeDisconnect(uuid, email)
|
defer xrayMgr.recordNativeDisconnect(uuid, email)
|
||||||
|
defer xrayRecover(fmt.Sprintf("native xray VMess UDP tunnel user=%s", email))
|
||||||
|
|
||||||
upMeter := &trafficMeter{uuid: uuid, email: email, uplink: true}
|
upMeter := &trafficMeter{uuid: uuid, email: email, uplink: true}
|
||||||
downMeter := &trafficMeter{uuid: uuid, email: email, uplink: false}
|
downMeter := &trafficMeter{uuid: uuid, email: email, uplink: false}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
var closeOnce sync.Once
|
||||||
closeAll := func() {
|
closeAll := func() {
|
||||||
_ = backend.Close()
|
closeOnce.Do(func() {
|
||||||
_ = client.Close()
|
_ = backend.Close()
|
||||||
|
_ = client.Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
xrayGo("native xray VMess UDP uplink", func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer closeAll()
|
||||||
for {
|
for {
|
||||||
pkt, err := client.ReadPacket()
|
pkt, err := client.ReadPacket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Printf("native xray: VMess UDP client read failed: %v", err)
|
xrayLogf("native xray: VMess UDP client read failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -331,15 +341,16 @@ func nativeVMessUDPTunnel(client nativeVMessStream, backend net.Conn, uuid, emai
|
|||||||
upMeter.add(n)
|
upMeter.add(n)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("native xray: VMess UDP backend write failed: %v", err)
|
xrayLogf("native xray: VMess UDP backend write failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
xrayGo("native xray VMess UDP downlink", func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer closeAll()
|
||||||
buf := make([]byte, nativeUDPBufferSize)
|
buf := make([]byte, nativeUDPBufferSize)
|
||||||
for {
|
for {
|
||||||
_ = backend.SetReadDeadline(time.Now().Add(nativeUDPIdle))
|
_ = backend.SetReadDeadline(time.Now().Add(nativeUDPIdle))
|
||||||
@@ -349,7 +360,7 @@ func nativeVMessUDPTunnel(client nativeVMessStream, backend net.Conn, uuid, emai
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Printf("native xray: VMess UDP backend read failed: %v", err)
|
xrayLogf("native xray: VMess UDP backend read failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -360,12 +371,12 @@ func nativeVMessUDPTunnel(client nativeVMessStream, backend net.Conn, uuid, emai
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := client.WritePacket(buf[:n]); err != nil {
|
if err := client.WritePacket(buf[:n]); err != nil {
|
||||||
log.Printf("native xray: VMess UDP client write failed: %v", err)
|
xrayLogf("native xray: VMess UDP client write failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
downMeter.add(n)
|
downMeter.add(n)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
upMeter.flush()
|
upMeter.flush()
|
||||||
|
|||||||
+190
-47
@@ -2,11 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
"container/heap"
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -98,7 +98,12 @@ func mergeNativeXHTTPSettings(primary, fallback nativeXHTTPSettingsJSON) nativeX
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) serveXHTTPListener(ln net.Listener) {
|
func (ib *nativeInbound) serveXHTTPListener(ln net.Listener) {
|
||||||
h2s := &http2.Server{}
|
defer xrayRecover(fmt.Sprintf("native xray XHTTP listener inbound=%q addr=%s", ib.tag, ln.Addr()))
|
||||||
|
h2s := &http2.Server{
|
||||||
|
MaxConcurrentStreams: uint32(nativeH2MaxConcurrentStreams()),
|
||||||
|
MaxUploadBufferPerConnection: int32(nativeH2UploadBufferConn()),
|
||||||
|
MaxUploadBufferPerStream: int32(nativeH2UploadBufferStream()),
|
||||||
|
}
|
||||||
handler := http.Handler(ib)
|
handler := http.Handler(ib)
|
||||||
// Official Xray accepts plaintext HTTP/1.1 and h2c on non-TLS XHTTP
|
// Official Xray accepts plaintext HTTP/1.1 and h2c on non-TLS XHTTP
|
||||||
// listeners, and negotiates h2/http1 through ALPN on TLS listeners. Without
|
// listeners, and negotiates h2/http1 through ALPN on TLS listeners. Without
|
||||||
@@ -117,7 +122,7 @@ func (ib *nativeInbound) serveXHTTPListener(ln net.Listener) {
|
|||||||
_ = http2.ConfigureServer(srv, h2s)
|
_ = http2.ConfigureServer(srv, h2s)
|
||||||
}
|
}
|
||||||
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) && !isListenerClosed(err) {
|
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) && !isListenerClosed(err) {
|
||||||
log.Printf("native xray: XHTTP server for inbound %q stopped: %v", ib.tag, err)
|
xrayLogf("native xray: XHTTP server for inbound %q stopped: %v", ib.tag, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,19 +138,20 @@ func (ib *nativeInbound) xhttpServerMaxHeaderBytes() int {
|
|||||||
// ServeHTTP terminates the XHTTP/SplitHTTP transport and exposes the decoded
|
// ServeHTTP terminates the XHTTP/SplitHTTP transport and exposes the decoded
|
||||||
// byte stream to the VLESS/VMess handlers as a net.Conn.
|
// byte stream to the VLESS/VMess handlers as a net.Conn.
|
||||||
func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer xrayRecover(fmt.Sprintf("native xray XHTTP request inbound=%q method=%s path=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.RemoteAddr))
|
||||||
if !ib.isXHTTP() {
|
if !ib.isXHTTP() {
|
||||||
log.Printf("native xray: xhttp reject inbound=%q reason=not-xhttp method=%s path=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, r.RemoteAddr)
|
xrayLogf("native xray: xhttp reject inbound=%q reason=not-xhttp method=%s path=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, r.RemoteAddr)
|
||||||
xhttpBadRequest(w)
|
xhttpBadRequest(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !ib.xhttpHostAllowed(r.Host) {
|
if !ib.xhttpHostAllowed(r.Host) {
|
||||||
log.Printf("native xray: xhttp reject inbound=%q reason=host method=%s path=%q host=%q want=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, ib.xhttpHost, r.RemoteAddr)
|
xrayLogf("native xray: xhttp reject inbound=%q reason=host method=%s path=%q host=%q want=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), r.Host, ib.xhttpHost, r.RemoteAddr)
|
||||||
xhttpBadRequest(w)
|
xhttpBadRequest(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
base, ok := ib.matchXHTTPPath(r.URL.Path)
|
base, ok := ib.matchXHTTPPath(r.URL.Path)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("native xray: xhttp reject inbound=%q reason=path method=%s path=%q want=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), ib.path, r.Host, r.RemoteAddr)
|
xrayLogf("native xray: xhttp reject inbound=%q reason=path method=%s path=%q want=%q host=%q remote=%s", ib.tag, r.Method, r.URL.RequestURI(), ib.path, r.Host, r.RemoteAddr)
|
||||||
xhttpBadRequest(w)
|
xhttpBadRequest(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -158,7 +164,7 @@ func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
sessionID, seqStr := ib.extractXHTTPMeta(r, base)
|
sessionID, seqStr := ib.extractXHTTPMeta(r, base)
|
||||||
mode := ib.normalizedXHTTPMode()
|
mode := ib.normalizedXHTTPMode()
|
||||||
log.Printf("native xray: xhttp request inbound=%q method=%s proto=%s path=%q host=%q session=%q seq=%q len=%d mode=%s remote=%s", ib.tag, r.Method, r.Proto, r.URL.RequestURI(), r.Host, sessionID, seqStr, r.ContentLength, mode, r.RemoteAddr)
|
xrayTracef("native xray: xhttp request inbound=%q method=%s proto=%s path=%q host=%q session=%q seq=%q len=%d mode=%s remote=%s", ib.tag, r.Method, r.Proto, r.URL.RequestURI(), r.Host, sessionID, seqStr, r.ContentLength, mode, r.RemoteAddr)
|
||||||
|
|
||||||
// Xray's SplitHTTP treats GET with a sequence id as an uplink packet, not as
|
// Xray's SplitHTTP treats GET with a sequence id as an uplink packet, not as
|
||||||
// stream-down. Some clients use this when the upload payload is carried in
|
// stream-down. Some clients use this when the upload payload is carried in
|
||||||
@@ -166,7 +172,10 @@ func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// treated GET as download and dropped those packets, so normal sites such as
|
// treated GET as download and dropped those packets, so normal sites such as
|
||||||
// fast.com could authenticate but then stall with no upstream data.
|
// fast.com could authenticate but then stall with no upstream data.
|
||||||
if r.Method == http.MethodGet && sessionID != "" && seqStr != "" {
|
if r.Method == http.MethodGet && sessionID != "" && seqStr != "" {
|
||||||
sess := ib.upsertXHTTPSession(sessionID)
|
sess := ib.upsertXHTTPSession(w, sessionID)
|
||||||
|
if sess == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
ib.handleXHTTPPacketUpload(w, r, sess, seqStr)
|
ib.handleXHTTPPacketUpload(w, r, sess, seqStr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -179,7 +188,10 @@ func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
xhttpBadRequest(w)
|
xhttpBadRequest(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sess := ib.upsertXHTTPSession(sessionID)
|
sess := ib.upsertXHTTPSession(w, sessionID)
|
||||||
|
if sess == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
ib.handleXHTTPDownload(w, r, sess, sessionID)
|
ib.handleXHTTPDownload(w, r, sess, sessionID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -203,7 +215,10 @@ func (ib *nativeInbound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := ib.upsertXHTTPSession(sessionID)
|
sess := ib.upsertXHTTPSession(w, sessionID)
|
||||||
|
if sess == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if seqStr == "" {
|
if seqStr == "" {
|
||||||
ib.handleXHTTPStreamUpload(w, r, sess)
|
ib.handleXHTTPStreamUpload(w, r, sess)
|
||||||
return
|
return
|
||||||
@@ -385,39 +400,70 @@ func extractXHTTPValue(r *http.Request, placement, key string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) upsertXHTTPSession(id string) *nativeXHTTPSession {
|
func (ib *nativeInbound) upsertXHTTPSession(w http.ResponseWriter, id string) *nativeXHTTPSession {
|
||||||
ib.xhttpMu.Lock()
|
ib.xhttpMu.Lock()
|
||||||
defer ib.xhttpMu.Unlock()
|
defer ib.xhttpMu.Unlock()
|
||||||
if ib.xhttpSessions == nil {
|
if ib.xhttpSessions == nil {
|
||||||
ib.xhttpSessions = make(map[string]*nativeXHTTPSession)
|
ib.xhttpSessions = make(map[string]*nativeXHTTPSession)
|
||||||
}
|
}
|
||||||
if s := ib.xhttpSessions[id]; s != nil {
|
if s := ib.xhttpSessions[id]; s != nil {
|
||||||
|
s.touch()
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
if max := ib.xhttpMaxActiveSessions(); max > 0 && len(ib.xhttpSessions) >= max {
|
||||||
|
xrayTracef("native xray: xhttp session rejected inbound=%q active=%d limit=%d", ib.tag, len(ib.xhttpSessions), max)
|
||||||
|
http.Error(w, "xhttp session limit reached", http.StatusTooManyRequests)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
s := &nativeXHTTPSession{
|
s := &nativeXHTTPSession{
|
||||||
id: id,
|
id: id,
|
||||||
queue: newNativeXHTTPUploadQueue(ib.xhttpMaxBufferedPosts),
|
queue: newNativeXHTTPUploadQueue(ib.xhttpMaxBufferedPosts),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
|
lastSeen: time.Now(),
|
||||||
}
|
}
|
||||||
ib.xhttpSessions[id] = s
|
ib.xhttpSessions[id] = s
|
||||||
log.Printf("native xray: xhttp session created inbound=%q session=%q", ib.tag, id)
|
xrayTracef("native xray: xhttp session created inbound=%q session=%q active=%d", ib.tag, id, len(ib.xhttpSessions))
|
||||||
go ib.reapUnconnectedXHTTPSession(id, s)
|
xrayGo(fmt.Sprintf("native xray XHTTP session reaper inbound=%q session=%q", ib.tag, id), func() { ib.reapUnconnectedXHTTPSession(id, s) })
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ib *nativeInbound) xhttpMaxActiveSessions() int {
|
||||||
|
if nativeXHTTPMaxSessionLimit() > 0 {
|
||||||
|
return nativeXHTTPMaxSessionLimit()
|
||||||
|
}
|
||||||
|
return defaultNativeXHTTPMaxSessions
|
||||||
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) reapUnconnectedXHTTPSession(id string, s *nativeXHTTPSession) {
|
func (ib *nativeInbound) reapUnconnectedXHTTPSession(id string, s *nativeXHTTPSession) {
|
||||||
t := time.NewTimer(30 * time.Second)
|
// Keep the cheap unconnected cleanup, but also reap stale sessions that never
|
||||||
defer t.Stop()
|
// receive their paired download/close because a mobile network or CDN path died.
|
||||||
select {
|
unconnected := time.NewTimer(20 * time.Second)
|
||||||
case <-t.C:
|
stale := time.NewTicker(30 * time.Second)
|
||||||
s.mu.Lock()
|
defer unconnected.Stop()
|
||||||
connected := s.connected
|
defer stale.Stop()
|
||||||
s.mu.Unlock()
|
for {
|
||||||
if !connected {
|
select {
|
||||||
ib.deleteXHTTPSession(id, s)
|
case <-unconnected.C:
|
||||||
s.close()
|
s.mu.Lock()
|
||||||
|
connected := s.connected
|
||||||
|
s.mu.Unlock()
|
||||||
|
if !connected {
|
||||||
|
ib.deleteXHTTPSession(id, s)
|
||||||
|
s.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-stale.C:
|
||||||
|
s.mu.Lock()
|
||||||
|
idle := time.Since(s.lastSeen)
|
||||||
|
s.mu.Unlock()
|
||||||
|
if idle > 5*time.Minute {
|
||||||
|
ib.deleteXHTTPSession(id, s)
|
||||||
|
s.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
case <-s.done:
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,13 +476,18 @@ func (ib *nativeInbound) deleteXHTTPSession(id string, s *nativeXHTTPSession) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) handleXHTTPStreamUpload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession) {
|
func (ib *nativeInbound) handleXHTTPStreamUpload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession) {
|
||||||
log.Printf("native xray: xhttp stream-up inbound=%q session=%q len=%d remote=%s", ib.tag, sess.id, r.ContentLength, r.RemoteAddr)
|
sess.touch()
|
||||||
|
xrayTracef("native xray: xhttp stream-up inbound=%q session=%q len=%d remote=%s", ib.tag, sess.id, r.ContentLength, r.RemoteAddr)
|
||||||
if ib.xhttpMode != "" && ib.xhttpMode != "auto" && ib.xhttpMode != "stream-up" && ib.xhttpMode != "stream-down" {
|
if ib.xhttpMode != "" && ib.xhttpMode != "auto" && ib.xhttpMode != "stream-up" && ib.xhttpMode != "stream-down" {
|
||||||
http.Error(w, "xhttp stream-up mode is not allowed", http.StatusBadRequest)
|
http.Error(w, "xhttp stream-up mode is not allowed", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := sess.queue.push(nativeXHTTPPacket{Reader: r.Body}); err != nil {
|
if err := sess.queue.pushContext(r.Context(), nativeXHTTPPacket{Reader: r.Body}, nativeXHTTPQueuePushTimeoutDuration()); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusConflict)
|
status := http.StatusConflict
|
||||||
|
if errors.Is(err, errNativeXHTTPQueueFull) {
|
||||||
|
status = http.StatusTooManyRequests
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), status)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("X-Accel-Buffering", "no")
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
@@ -450,6 +501,7 @@ func (ib *nativeInbound) handleXHTTPStreamUpload(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession, seqStr string) {
|
func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession, seqStr string) {
|
||||||
|
sess.touch()
|
||||||
if ib.xhttpMode != "" && ib.xhttpMode != "auto" && ib.xhttpMode != "packet-up" && ib.xhttpMode != "stream-down" {
|
if ib.xhttpMode != "" && ib.xhttpMode != "auto" && ib.xhttpMode != "packet-up" && ib.xhttpMode != "stream-down" {
|
||||||
http.Error(w, "xhttp packet-up mode is not allowed", http.StatusBadRequest)
|
http.Error(w, "xhttp packet-up mode is not allowed", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -464,10 +516,14 @@ func (ib *nativeInbound) handleXHTTPPacketUpload(w http.ResponseWriter, r *http.
|
|||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("native xray: xhttp packet-up inbound=%q session=%q seq=%d payload=%d remote=%s", ib.tag, sess.id, seq, len(payload), r.RemoteAddr)
|
xrayTracef("native xray: xhttp packet-up inbound=%q session=%q seq=%d payload=%d remote=%s", ib.tag, sess.id, seq, len(payload), r.RemoteAddr)
|
||||||
if err := sess.queue.push(nativeXHTTPPacket{Payload: payload, Seq: seq}); err != nil {
|
if err := sess.queue.pushContext(r.Context(), nativeXHTTPPacket{Payload: payload, Seq: seq}, nativeXHTTPQueuePushTimeoutDuration()); err != nil {
|
||||||
log.Printf("native xray: xhttp packet-up push failed inbound=%q session=%q seq=%d: %v", ib.tag, sess.id, seq, err)
|
status := http.StatusConflict
|
||||||
http.Error(w, err.Error(), http.StatusConflict)
|
if errors.Is(err, errNativeXHTTPQueueFull) {
|
||||||
|
status = http.StatusTooManyRequests
|
||||||
|
}
|
||||||
|
xrayTracef("native xray: xhttp packet-up push failed inbound=%q session=%q seq=%d: %v", ib.tag, sess.id, seq, err)
|
||||||
|
http.Error(w, err.Error(), status)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
@@ -576,7 +632,8 @@ func (ib *nativeInbound) xhttpMaxPostBytes() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) {
|
func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("native xray: xhttp stream-one inbound=%q len=%d remote=%s", ib.tag, r.ContentLength, r.RemoteAddr)
|
defer xrayRecover(fmt.Sprintf("native xray XHTTP stream-one inbound=%q remote=%s", ib.tag, r.RemoteAddr))
|
||||||
|
xrayTracef("native xray: xhttp stream-one inbound=%q len=%d remote=%s", ib.tag, r.ContentLength, r.RemoteAddr)
|
||||||
w.Header().Set("X-Accel-Buffering", "no")
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
if !ib.xhttpNoSSEHeader {
|
if !ib.xhttpNoSSEHeader {
|
||||||
@@ -586,12 +643,14 @@ func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Req
|
|||||||
flushHTTP(w)
|
flushHTTP(w)
|
||||||
|
|
||||||
remote := remoteAddrFromHTTPRequest(r)
|
remote := remoteAddrFromHTTPRequest(r)
|
||||||
|
resp := newNativeXHTTPResponseWriter(w)
|
||||||
xc := &nativeXHTTPConn{
|
xc := &nativeXHTTPConn{
|
||||||
reader: r.Body,
|
reader: r.Body,
|
||||||
writer: &nativeXHTTPResponseWriter{w: w},
|
writer: resp,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
local: dummyLocalAddr(r),
|
local: dummyLocalAddr(r),
|
||||||
onClose: func() {
|
onClose: func() {
|
||||||
|
resp.close()
|
||||||
_ = r.Body.Close()
|
_ = r.Body.Close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -600,7 +659,9 @@ func (ib *nativeInbound) handleXHTTPStreamOne(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) handleXHTTPDownload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession, sessionID string) {
|
func (ib *nativeInbound) handleXHTTPDownload(w http.ResponseWriter, r *http.Request, sess *nativeXHTTPSession, sessionID string) {
|
||||||
log.Printf("native xray: xhttp stream-down inbound=%q session=%q proto=%s remote=%s", ib.tag, sessionID, r.Proto, r.RemoteAddr)
|
sess.touch()
|
||||||
|
defer xrayRecover(fmt.Sprintf("native xray XHTTP download inbound=%q session=%q remote=%s", ib.tag, sessionID, r.RemoteAddr))
|
||||||
|
xrayTracef("native xray: xhttp stream-down inbound=%q session=%q proto=%s remote=%s", ib.tag, sessionID, r.Proto, r.RemoteAddr)
|
||||||
sess.markConnected()
|
sess.markConnected()
|
||||||
defer ib.deleteXHTTPSession(sessionID, sess)
|
defer ib.deleteXHTTPSession(sessionID, sess)
|
||||||
|
|
||||||
@@ -614,27 +675,31 @@ func (ib *nativeInbound) handleXHTTPDownload(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
remote := remoteAddrFromHTTPRequest(r)
|
remote := remoteAddrFromHTTPRequest(r)
|
||||||
var reader io.Reader = sess.queue
|
var reader io.Reader = sess.queue
|
||||||
|
resp := newNativeXHTTPResponseWriter(w)
|
||||||
xc := &nativeXHTTPConn{
|
xc := &nativeXHTTPConn{
|
||||||
reader: reader,
|
reader: reader,
|
||||||
writer: &nativeXHTTPResponseWriter{w: w},
|
writer: resp,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
local: dummyLocalAddr(r),
|
local: dummyLocalAddr(r),
|
||||||
}
|
}
|
||||||
xc.onClose = sess.close
|
xc.onClose = func() {
|
||||||
|
resp.close()
|
||||||
|
sess.close()
|
||||||
|
}
|
||||||
|
|
||||||
ib.dispatchXHTTPConn(xc, remote)
|
ib.dispatchXHTTPConn(xc, remote)
|
||||||
_ = xc.Close()
|
_ = xc.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ib *nativeInbound) dispatchXHTTPConn(xc net.Conn, remote net.Addr) {
|
func (ib *nativeInbound) dispatchXHTTPConn(xc net.Conn, remote net.Addr) {
|
||||||
log.Printf("native xray: xhttp dispatch inbound=%q protocol=%s remote=%s", ib.tag, ib.protocol, remote)
|
xrayTracef("native xray: xhttp dispatch inbound=%q protocol=%s remote=%s", ib.tag, ib.protocol, remote)
|
||||||
switch ib.protocol {
|
switch ib.protocol {
|
||||||
case "vless":
|
case "vless":
|
||||||
ib.handleVLESS(xc, remote)
|
ib.handleVLESS(xc, remote)
|
||||||
case "vmess":
|
case "vmess":
|
||||||
ib.handleVMess(xc, remote)
|
ib.handleVMess(xc, remote)
|
||||||
default:
|
default:
|
||||||
log.Printf("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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,11 +731,19 @@ type nativeXHTTPSession struct {
|
|||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
connected bool
|
connected bool
|
||||||
|
lastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *nativeXHTTPSession) touch() {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.lastSeen = time.Now()
|
||||||
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *nativeXHTTPSession) markConnected() {
|
func (s *nativeXHTTPSession) markConnected() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.connected = true
|
s.connected = true
|
||||||
|
s.lastSeen = time.Now()
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,9 +809,21 @@ func (c *nativeXHTTPConn) SetReadDeadline(t time.Time) error {
|
|||||||
func (c *nativeXHTTPConn) SetWriteDeadline(time.Time) error { return nil }
|
func (c *nativeXHTTPConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
type nativeXHTTPResponseWriter struct {
|
type nativeXHTTPResponseWriter struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
w http.ResponseWriter
|
w http.ResponseWriter
|
||||||
closed bool
|
closed bool
|
||||||
|
pendingFlush bool
|
||||||
|
lastFlush time.Time
|
||||||
|
buffered int
|
||||||
|
flushTimer *time.Timer
|
||||||
|
timerActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNativeXHTTPResponseWriter(w http.ResponseWriter) *nativeXHTTPResponseWriter {
|
||||||
|
// The handler writes/flushed headers before the proxy stream is dispatched.
|
||||||
|
// Starting lastFlush at now prevents the first tiny mux packet from forcing an
|
||||||
|
// immediate extra flush for every user.
|
||||||
|
return &nativeXHTTPResponseWriter{w: w, lastFlush: time.Now()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *nativeXHTTPResponseWriter) Write(p []byte) (int, error) {
|
func (w *nativeXHTTPResponseWriter) Write(p []byte) (int, error) {
|
||||||
@@ -748,15 +833,62 @@ func (w *nativeXHTTPResponseWriter) Write(p []byte) (int, error) {
|
|||||||
return 0, io.ErrClosedPipe
|
return 0, io.ErrClosedPipe
|
||||||
}
|
}
|
||||||
n, err := w.w.Write(p)
|
n, err := w.w.Write(p)
|
||||||
|
if n > 0 {
|
||||||
|
w.buffered += n
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
flushHTTP(w.w)
|
w.flushMaybeLocked(false)
|
||||||
}
|
}
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *nativeXHTTPResponseWriter) flushMaybeLocked(force bool) {
|
||||||
|
now := time.Now()
|
||||||
|
if force || w.buffered >= nativeXHTTPFlushByteLimit() || now.Sub(w.lastFlush) >= nativeXHTTPFlushIntervalDuration() {
|
||||||
|
flushHTTP(w.w)
|
||||||
|
w.lastFlush = now
|
||||||
|
w.buffered = 0
|
||||||
|
w.pendingFlush = false
|
||||||
|
w.timerActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w.pendingFlush {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.pendingFlush = true
|
||||||
|
if w.timerActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.timerActive = true
|
||||||
|
if w.flushTimer == nil {
|
||||||
|
w.flushTimer = time.AfterFunc(nativeXHTTPFlushIntervalDuration(), w.fireFlushTimer)
|
||||||
|
} else {
|
||||||
|
w.flushTimer.Reset(nativeXHTTPFlushIntervalDuration())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *nativeXHTTPResponseWriter) fireFlushTimer() {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.timerActive = false
|
||||||
|
if w.closed || !w.pendingFlush {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flushHTTP(w.w)
|
||||||
|
w.lastFlush = time.Now()
|
||||||
|
w.buffered = 0
|
||||||
|
w.pendingFlush = false
|
||||||
|
}
|
||||||
|
|
||||||
func (w *nativeXHTTPResponseWriter) close() {
|
func (w *nativeXHTTPResponseWriter) close() {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
w.closed = true
|
if !w.closed {
|
||||||
|
if w.flushTimer != nil {
|
||||||
|
w.flushTimer.Stop()
|
||||||
|
}
|
||||||
|
w.flushMaybeLocked(true)
|
||||||
|
w.closed = true
|
||||||
|
}
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,12 +921,23 @@ func newNativeXHTTPUploadQueue(maxPackets int) *nativeXHTTPUploadQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *nativeXHTTPUploadQueue) push(p nativeXHTTPPacket) error {
|
var errNativeXHTTPQueueFull = errors.New("xhttp upload queue full")
|
||||||
|
|
||||||
|
func (q *nativeXHTTPUploadQueue) pushContext(ctx context.Context, p nativeXHTTPPacket, timeout time.Duration) error {
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = nativeXHTTPQueuePushTimeoutDuration()
|
||||||
|
}
|
||||||
|
t := time.NewTimer(timeout)
|
||||||
|
defer t.Stop()
|
||||||
select {
|
select {
|
||||||
case q.pushedPackets <- p:
|
case q.pushedPackets <- p:
|
||||||
return nil
|
return nil
|
||||||
case <-q.closed:
|
case <-q.closed:
|
||||||
return io.ErrClosedPipe
|
return io.ErrClosedPipe
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-t.C:
|
||||||
|
return errNativeXHTTPQueueFull
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user