mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-14 07:10:58 +00:00
fix: expose streamSettings for Tunnel inbounds to support TProxy (#5171)
* fix: expose streamSettings for Tunnel inbounds to support TProxy * fix(ui): hide security tab for tunnel inbounds when stream is enabled tunnel (dokodemo-door) does not support TLS or Reality, so showing the security tab only results in a fully-disabled radio group. Exclude tunnel alongside wireguard from the security tab. * fix(tunnel): restrict stream tab to sockopt-only and fix transportless schema Tunnel (dokodemo-door) only needs sockopt.tproxy for TProxy mode — no user-selectable transport. Add hasSelectableTransport flag to hide the network picker, per-network sub-forms, ExternalProxy, and FinalMask for both tunnel and wireguard, matching the pattern already used for Hysteria. Fix a pre-existing Zod schema bug where NetworkSettingsSchema was a bare discriminatedUnion requiring `network` to be present. Wireguard and tunnel submit streamSettings without a `network` key, causing "Invalid discriminator value. Expected 'tcp' | ..." on every save. Fix by adding a transportless union branch (z.never().optional()) alongside the transport DU; also add ?? 'tcp' fallback in inbound-link.ts where stream.network is now string | undefined. Three regression tests added. --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -1179,7 +1179,7 @@ exports[`protocol capability predicates > trojan-basic :: xhttp/tls 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: grpc/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1191,7 +1191,7 @@ exports[`protocol capability predicates > tunnel-basic :: grpc/none 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: grpc/reality 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1203,7 +1203,7 @@ exports[`protocol capability predicates > tunnel-basic :: grpc/reality 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: grpc/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1215,7 +1215,7 @@ exports[`protocol capability predicates > tunnel-basic :: grpc/tls 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: httpupgrade/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1227,7 +1227,7 @@ exports[`protocol capability predicates > tunnel-basic :: httpupgrade/none 1`] =
|
||||
exports[`protocol capability predicates > tunnel-basic :: httpupgrade/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1239,7 +1239,7 @@ exports[`protocol capability predicates > tunnel-basic :: httpupgrade/tls 1`] =
|
||||
exports[`protocol capability predicates > tunnel-basic :: kcp/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1251,7 +1251,7 @@ exports[`protocol capability predicates > tunnel-basic :: kcp/none 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: tcp/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1263,7 +1263,7 @@ exports[`protocol capability predicates > tunnel-basic :: tcp/none 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: tcp/reality 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1275,7 +1275,7 @@ exports[`protocol capability predicates > tunnel-basic :: tcp/reality 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: tcp/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1287,7 +1287,7 @@ exports[`protocol capability predicates > tunnel-basic :: tcp/tls 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: ws/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1299,7 +1299,7 @@ exports[`protocol capability predicates > tunnel-basic :: ws/none 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: ws/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1311,7 +1311,7 @@ exports[`protocol capability predicates > tunnel-basic :: ws/tls 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: xhttp/none 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1323,7 +1323,7 @@ exports[`protocol capability predicates > tunnel-basic :: xhttp/none 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: xhttp/reality 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
@@ -1335,7 +1335,7 @@ exports[`protocol capability predicates > tunnel-basic :: xhttp/reality 1`] = `
|
||||
exports[`protocol capability predicates > tunnel-basic :: xhttp/tls 1`] = `
|
||||
{
|
||||
"canEnableReality": false,
|
||||
"canEnableStream": false,
|
||||
"canEnableStream": true,
|
||||
"canEnableTls": false,
|
||||
"canEnableTlsFlow": false,
|
||||
"canEnableVisionSeed": false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type RawInboundRow,
|
||||
} from '@/lib/xray/inbound-form-adapter';
|
||||
import { InboundFormSchema } from '@/schemas/forms/inbound-form';
|
||||
import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
|
||||
|
||||
// Round-trip: raw DB row → InboundFormValues → wire payload, asserting
|
||||
// that the JSON-stringified settings/streamSettings/sniffing in the
|
||||
@@ -113,6 +114,55 @@ describe('rawInboundToFormValues', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Regression: wireguard (UDP-only) and tunnel (dokodemo-door) have no
|
||||
// user-selectable transport, so the modal submits streamSettings WITHOUT a
|
||||
// `network` key — just `security`, plus `sockopt` for tunnel's TProxy. The
|
||||
// network schema must accept that transportless shape; before the transportless
|
||||
// union branch landed it failed with "Invalid discriminator value. Expected
|
||||
// 'tcp' | ..." and blocked every wireguard/tunnel save.
|
||||
describe('transportless streamSettings (wireguard / tunnel)', () => {
|
||||
it('accepts wireguard with a network-less streamSettings', () => {
|
||||
const result = InboundFormSchema.safeParse({
|
||||
port: 51820,
|
||||
protocol: 'wireguard',
|
||||
settings: { secretKey: 'cE9mYWtlLXNlY3JldC1rZXktZm9yLXVuaXQtdGVzdA==', peers: [] },
|
||||
streamSettings: { security: 'none' },
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts tunnel with sockopt.tproxy and no network', () => {
|
||||
const result = InboundFormSchema.safeParse({
|
||||
port: 12345,
|
||||
protocol: 'tunnel',
|
||||
settings: { allowedNetwork: 'tcp,udp', followRedirect: true, portMap: {} },
|
||||
streamSettings: {
|
||||
security: 'none',
|
||||
sockopt: SockoptStreamSettingsSchema.parse({ tproxy: 'tproxy' }),
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
const stream = result.data.streamSettings as {
|
||||
network?: unknown;
|
||||
sockopt?: { tproxy?: string };
|
||||
};
|
||||
expect(stream.network).toBeUndefined();
|
||||
expect(stream.sockopt?.tproxy).toBe('tproxy');
|
||||
}
|
||||
});
|
||||
|
||||
it('still rejects a present-but-invalid network value', () => {
|
||||
const result = InboundFormSchema.safeParse({
|
||||
port: 12345,
|
||||
protocol: 'tunnel',
|
||||
settings: { allowedNetwork: 'tcp,udp', followRedirect: true, portMap: {} },
|
||||
streamSettings: { network: 'bogus', security: 'none' },
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formValuesToWirePayload', () => {
|
||||
it('stringifies settings/streamSettings/sniffing with empty-array/default pruning', () => {
|
||||
const values = rawInboundToFormValues(vlessRow);
|
||||
|
||||
Reference in New Issue
Block a user