mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a79cb9fe6d
commit
fc8765917e
6 changed files with 352 additions and 399 deletions
|
|
@ -2570,7 +2570,7 @@ Inbound.ClientBase = class extends XrayCommonClass {
|
|||
|
||||
Inbound.VmessSettings = class extends Inbound.Settings {
|
||||
constructor(protocol,
|
||||
vmesses = [new Inbound.VmessSettings.VMESS()]) {
|
||||
vmesses = []) {
|
||||
super(protocol);
|
||||
this.vmesses = vmesses;
|
||||
}
|
||||
|
|
@ -2638,7 +2638,7 @@ Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase {
|
|||
Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
constructor(
|
||||
protocol,
|
||||
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
||||
vlesses = [],
|
||||
decryption = "none",
|
||||
encryption = "none",
|
||||
fallbacks = [],
|
||||
|
|
@ -2785,7 +2785,7 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
|
|||
|
||||
Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||
constructor(protocol,
|
||||
trojans = [new Inbound.TrojanSettings.Trojan()],
|
||||
trojans = [],
|
||||
fallbacks = [],) {
|
||||
super(protocol);
|
||||
this.trojans = trojans;
|
||||
|
|
@ -2868,7 +2868,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
|
|||
method = SSMethods.BLAKE3_AES_256_GCM,
|
||||
password = RandomUtil.randomShadowsocksPassword(),
|
||||
network = 'tcp,udp',
|
||||
shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()],
|
||||
shadowsockses = [],
|
||||
ivCheck = false,
|
||||
) {
|
||||
super(protocol);
|
||||
|
|
@ -2930,7 +2930,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase {
|
|||
};
|
||||
|
||||
Inbound.HysteriaSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, version = 2, hysterias = [new Inbound.HysteriaSettings.Hysteria()]) {
|
||||
constructor(protocol, version = 2, hysterias = []) {
|
||||
super(protocol);
|
||||
this.version = version;
|
||||
this.hysterias = hysterias;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,17 @@ import { message } from 'ant-design-vue';
|
|||
|
||||
import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils';
|
||||
import DateTimePicker from '@/components/DateTimePicker.vue';
|
||||
import { DBInbound } from '@/models/dbinbound.js';
|
||||
import { TLS_FLOW_CONTROL } from '@/models/inbound.js';
|
||||
|
||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
inbounds: { type: Array, default: () => [] },
|
||||
ipLimitEnable: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:open', 'saved']);
|
||||
|
|
@ -31,12 +36,32 @@ const form = reactive({
|
|||
quantity: 1,
|
||||
subId: '',
|
||||
comment: '',
|
||||
flow: '',
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
inboundIds: [],
|
||||
});
|
||||
|
||||
const flowCapableIds = computed(() => {
|
||||
const ids = new Set();
|
||||
for (const row of props.inbounds || []) {
|
||||
try {
|
||||
const parsed = new DBInbound(row).toInbound();
|
||||
if (parsed.canEnableTlsFlow?.()) ids.add(row.id);
|
||||
} catch (_e) { /* ignore */ }
|
||||
}
|
||||
return ids;
|
||||
});
|
||||
|
||||
const showFlow = computed(() =>
|
||||
(form.inboundIds || []).some((id) => flowCapableIds.value.has(id)),
|
||||
);
|
||||
|
||||
watch(showFlow, (next) => {
|
||||
if (!next) form.flow = '';
|
||||
});
|
||||
|
||||
const expiryDate = computed({
|
||||
get: () => (form.expiryTime > 0 ? dayjs(form.expiryTime) : null),
|
||||
set: (next) => { form.expiryTime = next ? next.valueOf() : 0; },
|
||||
|
|
@ -64,6 +89,7 @@ watch(() => props.open, (next) => {
|
|||
form.quantity = 1;
|
||||
form.subId = '';
|
||||
form.comment = '';
|
||||
form.flow = '';
|
||||
form.limitIp = 0;
|
||||
form.totalGB = 0;
|
||||
form.expiryTime = 0;
|
||||
|
|
@ -118,6 +144,7 @@ async function submit() {
|
|||
id: RandomUtil.randomUUID(),
|
||||
password: RandomUtil.randomLowerAndNum(16),
|
||||
auth: RandomUtil.randomLowerAndNum(16),
|
||||
flow: showFlow.value ? (form.flow || '') : '',
|
||||
totalGB: Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB),
|
||||
expiryTime: form.expiryTime,
|
||||
limitIp: Number(form.limitIp) || 0,
|
||||
|
|
@ -191,8 +218,15 @@ async function submit() {
|
|||
<a-input v-model:value="form.comment" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="showFlow" label="Flow">
|
||||
<a-select v-model:value="form.flow" :style="{ width: '220px' }">
|
||||
<a-select-option value="">none</a-select-option>
|
||||
<a-select-option v-for="k in FLOW_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('pages.clients.limitIp') || 'IP Limit'">
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" />
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" :disabled="!ipLimitEnable" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('pages.clients.totalGB') || 'Total (GB)'">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { computed, reactive, ref, watch } from 'vue';
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import { message } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { RandomUtil } from '@/utils';
|
||||
import { HttpUtil, RandomUtil } from '@/utils';
|
||||
import { DBInbound } from '@/models/dbinbound.js';
|
||||
import { TLS_FLOW_CONTROL } from '@/models/inbound.js';
|
||||
|
||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
|
|
@ -11,6 +15,7 @@ const props = defineProps({
|
|||
client: { type: Object, default: null },
|
||||
inbounds: { type: Array, default: () => [] },
|
||||
attachedIds: { type: Array, default: () => [] },
|
||||
ipLimitEnable: { type: Boolean, default: false },
|
||||
save: { type: Function, required: true },
|
||||
});
|
||||
|
||||
|
|
@ -27,6 +32,7 @@ function emptyForm() {
|
|||
uuid: '',
|
||||
password: '',
|
||||
auth: '',
|
||||
flow: '',
|
||||
totalGB: 0,
|
||||
expiryTime: null,
|
||||
limitIp: 0,
|
||||
|
|
@ -49,12 +55,14 @@ watch(
|
|||
form.uuid = props.client.uuid || '';
|
||||
form.password = props.client.password || '';
|
||||
form.auth = props.client.auth || '';
|
||||
form.flow = props.client.flow || '';
|
||||
form.totalGB = bytesToGB(props.client.totalGB || 0);
|
||||
form.expiryTime = props.client.expiryTime ? dayjs(props.client.expiryTime) : null;
|
||||
form.limitIp = props.client.limitIp || 0;
|
||||
form.comment = props.client.comment || '';
|
||||
form.enable = !!props.client.enable;
|
||||
form.inboundIds = Array.isArray(props.attachedIds) ? [...props.attachedIds] : [];
|
||||
void loadIps();
|
||||
} else {
|
||||
form.uuid = RandomUtil.randomUUID();
|
||||
form.subId = RandomUtil.randomLowerAndNum(16);
|
||||
|
|
@ -82,6 +90,53 @@ const inboundOptions = computed(() =>
|
|||
})),
|
||||
);
|
||||
|
||||
const flowCapableIds = computed(() => {
|
||||
const ids = new Set();
|
||||
for (const row of props.inbounds || []) {
|
||||
try {
|
||||
const parsed = new DBInbound(row).toInbound();
|
||||
if (parsed.canEnableTlsFlow?.()) ids.add(row.id);
|
||||
} catch (_e) { /* ignore unparsable */ }
|
||||
}
|
||||
return ids;
|
||||
});
|
||||
|
||||
const showFlow = computed(() =>
|
||||
(form.inboundIds || []).some((id) => flowCapableIds.value.has(id)),
|
||||
);
|
||||
|
||||
watch(showFlow, (next) => {
|
||||
if (!next) form.flow = '';
|
||||
});
|
||||
|
||||
const clientIps = ref([]);
|
||||
const ipsLoading = ref(false);
|
||||
const ipsClearing = ref(false);
|
||||
|
||||
async function loadIps() {
|
||||
if (!isEdit.value || !props.client?.email) return;
|
||||
ipsLoading.value = true;
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/api/clients/ips/${encodeURIComponent(props.client.email)}`);
|
||||
if (!msg?.success) { clientIps.value = []; return; }
|
||||
const arr = Array.isArray(msg.obj) ? msg.obj : [];
|
||||
clientIps.value = arr.filter((x) => typeof x === 'string' && x.length > 0);
|
||||
} finally {
|
||||
ipsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearIps() {
|
||||
if (!isEdit.value || !props.client?.email) return;
|
||||
ipsClearing.value = true;
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/api/clients/clearIps/${encodeURIComponent(props.client.email)}`);
|
||||
if (msg?.success) clientIps.value = [];
|
||||
} finally {
|
||||
ipsClearing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('update:open', false);
|
||||
}
|
||||
|
|
@ -121,6 +176,7 @@ async function onSubmit() {
|
|||
id: form.uuid,
|
||||
password: form.password,
|
||||
auth: form.auth,
|
||||
flow: showFlow.value ? (form.flow || '') : '',
|
||||
totalGB: gbToBytes(form.totalGB),
|
||||
expiryTime: form.expiryTime ? form.expiryTime.valueOf() : 0,
|
||||
limitIp: Number(form.limitIp) || 0,
|
||||
|
|
@ -209,7 +265,18 @@ async function onSubmit() {
|
|||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.clients.limitIp') || 'IP limit'">
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" style="width: 100%" />
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" :disabled="!ipLimitEnable" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row v-if="showFlow" :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="Flow">
|
||||
<a-select v-model:value="form.flow">
|
||||
<a-select-option value="">none</a-select-option>
|
||||
<a-select-option v-for="k in FLOW_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
|
@ -241,6 +308,19 @@ async function onSubmit() {
|
|||
<a-switch v-model:checked="form.enable" />
|
||||
<span style="margin-left: 8px">{{ t('enable') }}</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="isEdit" :label="t('pages.inbounds.ipLog') || 'IP Log'">
|
||||
<a-space style="margin-bottom: 8px">
|
||||
<a-button size="small" :loading="ipsLoading" @click="loadIps">{{ t('refresh') }}</a-button>
|
||||
<a-button size="small" danger :loading="ipsClearing" :disabled="clientIps.length === 0" @click="clearIps">
|
||||
{{ t('clearAll') || 'Clear' }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
<div v-if="clientIps.length > 0">
|
||||
<a-tag v-for="(ip, idx) in clientIps" :key="idx" color="blue" style="margin-bottom: 4px">{{ ip }}</a-tag>
|
||||
</div>
|
||||
<a-tag v-else>{{ t('tgbot.noIpRecord') || 'No IP record' }}</a-tag>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const {
|
|||
loading,
|
||||
fetched,
|
||||
subSettings,
|
||||
ipLimitEnable,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
|
|
@ -516,11 +517,12 @@ const columns = computed(() => [
|
|||
</a-layout>
|
||||
|
||||
<ClientFormModal v-model:open="formOpen" :mode="formMode" :client="editingClient"
|
||||
:attached-ids="editingAttachedIds" :inbounds="inbounds" :save="onSave" />
|
||||
:attached-ids="editingAttachedIds" :inbounds="inbounds" :ip-limit-enable="ipLimitEnable" :save="onSave" />
|
||||
<ClientInfoModal v-model:open="infoOpen" :client="infoClient" :inbounds-by-id="inboundsById"
|
||||
:is-online="infoClient ? isOnline(infoClient.email) : false" :sub-settings="subSettings" />
|
||||
<ClientQrModal v-model:open="qrOpen" :client="qrClient" :sub-settings="subSettings" />
|
||||
<ClientBulkAddModal v-model:open="bulkAddOpen" :inbounds="inbounds" @saved="onBulkAddSaved" />
|
||||
<ClientBulkAddModal v-model:open="bulkAddOpen" :inbounds="inbounds" :ip-limit-enable="ipLimitEnable"
|
||||
@saved="onBulkAddSaved" />
|
||||
</a-layout>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export function useClients() {
|
|||
const loading = ref(false);
|
||||
const fetched = ref(false);
|
||||
const subSettings = ref({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false });
|
||||
const ipLimitEnable = ref(false);
|
||||
let onlinesTimer = null;
|
||||
|
||||
async function refresh() {
|
||||
|
|
@ -42,6 +43,7 @@ export function useClients() {
|
|||
subJsonURI: s.subJsonURI || '',
|
||||
subJsonEnable: !!s.subJsonEnable,
|
||||
};
|
||||
ipLimitEnable.value = !!s.ipLimitEnable;
|
||||
}
|
||||
|
||||
async function refreshOnlines() {
|
||||
|
|
@ -138,6 +140,7 @@ export function useClients() {
|
|||
loading,
|
||||
fetched,
|
||||
subSettings,
|
||||
ipLimitEnable,
|
||||
refresh,
|
||||
refreshOnlines,
|
||||
create,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ import {
|
|||
Inbound,
|
||||
Protocols,
|
||||
SSMethods,
|
||||
USERS_SECURITY,
|
||||
TLS_FLOW_CONTROL,
|
||||
SNIFFING_OPTION,
|
||||
TLS_VERSION_OPTION,
|
||||
TLS_CIPHER_OPTION,
|
||||
|
|
@ -63,8 +61,6 @@ const emit = defineEmits(['update:open', 'saved']);
|
|||
|
||||
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'];
|
||||
const PROTOCOLS = Object.values(Protocols);
|
||||
const SECURITY_OPTIONS = Object.values(USERS_SECURITY);
|
||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||
|
||||
// === Reactive state ================================================
|
||||
// Cloned on every open so cancelling the modal doesn't mutate the row.
|
||||
|
|
@ -123,35 +119,6 @@ const security = computed({
|
|||
set: (v) => { if (inbound.value?.stream) inbound.value.stream.security = v; },
|
||||
});
|
||||
|
||||
const isMultiUser = computed(() => {
|
||||
if (!inbound.value) return false;
|
||||
switch (inbound.value.protocol) {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.PORTFALLBACK:
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.HYSTERIA:
|
||||
return true;
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return !!inbound.value.isSSMultiUser;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const clientsArray = computed(() => {
|
||||
if (!inbound.value) return [];
|
||||
switch (inbound.value.protocol) {
|
||||
case Protocols.VMESS: return inbound.value.settings.vmesses || [];
|
||||
case Protocols.VLESS:
|
||||
case Protocols.PORTFALLBACK: return inbound.value.settings.vlesses || [];
|
||||
case Protocols.TROJAN: return inbound.value.settings.trojans || [];
|
||||
case Protocols.SHADOWSOCKS: return inbound.value.settings.shadowsockses || [];
|
||||
case Protocols.HYSTERIA: return inbound.value.settings.hysterias || [];
|
||||
default: return [];
|
||||
}
|
||||
});
|
||||
|
||||
const isVlessLike = computed(() => {
|
||||
if (!inbound.value) return false;
|
||||
return inbound.value.protocol === Protocols.VLESS
|
||||
|
|
@ -233,11 +200,9 @@ async function saveFallbackChildren(masterId) {
|
|||
return !!msg?.success;
|
||||
}
|
||||
|
||||
const firstClient = computed(() => clientsArray.value[0] || null);
|
||||
const canEnableStream = computed(() => inbound.value?.canEnableStream?.() === true);
|
||||
const canEnableTls = computed(() => inbound.value?.canEnableTls?.() === true);
|
||||
const canEnableReality = computed(() => inbound.value?.canEnableReality?.() === true);
|
||||
const canEnableTlsFlow = computed(() => inbound.value?.canEnableTlsFlow?.() === true);
|
||||
|
||||
// VLESS/Trojan TLS fallbacks — surfaced in the protocol tab when the
|
||||
// inbound is on TCP and (for VLESS) using no Xray-side encryption.
|
||||
|
|
@ -251,6 +216,23 @@ const showFallbacks = computed(() => {
|
|||
return inbound.value.protocol === Protocols.TROJAN;
|
||||
});
|
||||
|
||||
const hasProtocolTabContent = computed(() => {
|
||||
if (!inbound.value) return false;
|
||||
if (isVlessLike.value) return true;
|
||||
if (showFallbacks.value) return true;
|
||||
switch (inbound.value.protocol) {
|
||||
case Protocols.SHADOWSOCKS:
|
||||
case Protocols.HTTP:
|
||||
case Protocols.MIXED:
|
||||
case Protocols.TUNNEL:
|
||||
case Protocols.TUN:
|
||||
case Protocols.WIREGUARD:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
function addFallback() {
|
||||
inbound.value?.settings?.addFallback?.();
|
||||
}
|
||||
|
|
@ -268,16 +250,6 @@ const totalGB = computed({
|
|||
set: (gb) => { if (dbForm.value) dbForm.value.total = NumberFormatter.toFixed((gb || 0) * SizeFormatter.ONE_GB, 0); },
|
||||
});
|
||||
|
||||
// Client total/expiry bridges (only relevant in add mode for new clients)
|
||||
const clientExpiryDate = computed({
|
||||
get: () => (firstClient.value?.expiryTime > 0 ? dayjs(firstClient.value.expiryTime) : null),
|
||||
set: (next) => { if (firstClient.value) firstClient.value.expiryTime = next ? next.valueOf() : 0; },
|
||||
});
|
||||
const clientTotalGB = computed({
|
||||
get: () => firstClient.value?._totalGB ?? 0,
|
||||
set: (gb) => { if (firstClient.value) firstClient.value._totalGB = gb || 0; },
|
||||
});
|
||||
|
||||
// === Open / state management =======================================
|
||||
function loadFromDbInbound(dbIn) {
|
||||
// Round-trip through Inbound.fromJson so subsequent edits get the
|
||||
|
|
@ -371,6 +343,12 @@ watch(activeTabKey, (next, prev) => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(hasProtocolTabContent, (next) => {
|
||||
if (!next && activeTabKey.value === 'protocol') {
|
||||
activeTabKey.value = 'basic';
|
||||
}
|
||||
});
|
||||
|
||||
// In add mode, switching protocol restamps settings + re-syncs port.
|
||||
function onProtocolChange(next) {
|
||||
if (props.mode === 'edit' || !inbound.value) return;
|
||||
|
|
@ -573,25 +551,9 @@ const advancedStreamConfig = makeWrappedAdvancedConfig({
|
|||
label: 'Stream',
|
||||
});
|
||||
|
||||
// === Random helpers wired to the form's sync icons ==================
|
||||
function randomEmail(target) {
|
||||
if (target) target.email = RandomUtil.randomLowerAndNum(9);
|
||||
}
|
||||
function randomUuid(target) {
|
||||
if (target) target.id = RandomUtil.randomUUID();
|
||||
}
|
||||
function randomPasswordSeq(target, len = 10) {
|
||||
if (target) target.password = RandomUtil.randomSeq(len);
|
||||
}
|
||||
function randomSSPassword(target) {
|
||||
if (target) target.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
|
||||
}
|
||||
function randomAuth(target) {
|
||||
if (target) target.auth = RandomUtil.randomSeq(10);
|
||||
}
|
||||
function randomSubId(target) {
|
||||
if (target) target.subId = RandomUtil.randomLowerAndNum(16);
|
||||
}
|
||||
function regenWgKeypair(target) {
|
||||
const kp = Wireguard.generateKeypair();
|
||||
target.publicKey = kp.publicKey;
|
||||
|
|
@ -730,13 +692,9 @@ const selectedVlessAuth = computed(() => {
|
|||
return authKey.length > 300 ? 'ML-KEM-768 auth' : 'X25519 auth';
|
||||
});
|
||||
|
||||
// === SS method change tracks legacy semantics =========================
|
||||
function onSSMethodChange() {
|
||||
inbound.value.settings.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
|
||||
if (inbound.value.isSSMultiUser) {
|
||||
if (inbound.value.settings.shadowsockses.length === 0) {
|
||||
inbound.value.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||
}
|
||||
inbound.value.settings.shadowsockses.forEach((c) => {
|
||||
c.method = inbound.value.isSS2022 ? '' : inbound.value.settings.method;
|
||||
c.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
|
||||
|
|
@ -897,119 +855,7 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
|
|||
</a-tab-pane>
|
||||
|
||||
<!-- ============================== PROTOCOL ============================== -->
|
||||
<a-tab-pane key="protocol" :tab="t('pages.inbounds.protocol')">
|
||||
<!-- Multi-user inbounds: in add mode embed the first client form,
|
||||
in edit mode show a count summary. -->
|
||||
<template v-if="isMultiUser">
|
||||
<a-collapse v-if="mode === 'add' && firstClient" default-active-key="0">
|
||||
<a-collapse-panel key="0" header="Client">
|
||||
<a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
|
||||
<a-form-item label="Enable">
|
||||
<a-switch v-model:checked="firstClient.enable" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<a-tooltip title="Friendly identifier">
|
||||
Email
|
||||
<SyncOutlined class="random-icon" @click="randomEmail(firstClient)" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="firstClient.email" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="protocol === Protocols.VMESS || isVlessLike">
|
||||
<template #label>
|
||||
<a-tooltip title="Reset to a fresh UUID">
|
||||
ID
|
||||
<SyncOutlined class="random-icon" @click="randomUuid(firstClient)" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="firstClient.id" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="protocol === Protocols.VMESS" label="Security">
|
||||
<a-select v-model:value="firstClient.security">
|
||||
<a-select-option v-for="k in SECURITY_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS">
|
||||
<template #label>
|
||||
<a-tooltip title="Reset to a fresh random value">
|
||||
Password
|
||||
<SyncOutlined v-if="protocol === Protocols.SHADOWSOCKS" class="random-icon"
|
||||
@click="randomSSPassword(firstClient)" />
|
||||
<SyncOutlined v-else class="random-icon" @click="randomPasswordSeq(firstClient)" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="firstClient.password" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="protocol === Protocols.HYSTERIA">
|
||||
<template #label>
|
||||
<a-tooltip title="Reset"><span>Auth password</span>
|
||||
<SyncOutlined class="random-icon" @click="randomAuth(firstClient)" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="firstClient.auth" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="canEnableTlsFlow" label="Flow">
|
||||
<a-select v-model:value="firstClient.flow">
|
||||
<a-select-option value="">none</a-select-option>
|
||||
<a-select-option v-for="k in FLOW_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="protocol === Protocols.VLESS" label="Reverse tag">
|
||||
<a-input v-model:value="firstClient.reverseTag" placeholder="Optional reverse tag" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Subscription">
|
||||
<a-input v-model:value="firstClient.subId">
|
||||
<template #addonAfter>
|
||||
<SyncOutlined class="random-icon" @click="randomSubId(firstClient)" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Comment">
|
||||
<a-input v-model:value="firstClient.comment" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Total traffic (GB)">
|
||||
<a-input-number v-model:value="clientTotalGB" :min="0" :step="0.1" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Expiry">
|
||||
<DateTimePicker v-model:value="clientExpiryDate" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel key="summary" :header="`Clients: ${clientsArray.length}`">
|
||||
<table class="client-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>{{ protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS ? 'Password' : (protocol
|
||||
===
|
||||
Protocols.HYSTERIA ? 'Auth' : 'ID') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(c, idx) in clientsArray" :key="idx">
|
||||
<td>{{ c.email }}</td>
|
||||
<td>{{ c.id || c.password || c.auth }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
|
||||
<a-tab-pane v-if="hasProtocolTabContent" key="protocol" :tab="t('pages.inbounds.protocol')">
|
||||
<!-- VLess decryption / encryption -->
|
||||
<a-form v-if="isVlessLike" :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }"
|
||||
class="mt-12">
|
||||
|
|
@ -1772,205 +1618,6 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
|
|||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- ====== Security section ====== -->
|
||||
<a-form-item label="Security">
|
||||
<a-select v-model:value="security" :style="{ width: '160px' }" :disabled="!canEnableTls">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="tls">tls</a-select-option>
|
||||
<a-select-option v-if="canEnableReality" value="reality">reality</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="security === 'tls' && inbound.stream.tls">
|
||||
<a-form-item label="SNI">
|
||||
<a-input v-model:value="inbound.stream.tls.sni" placeholder="Server Name Indication" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Cipher Suites">
|
||||
<a-select v-model:value="inbound.stream.tls.cipherSuites">
|
||||
<a-select-option value="">Auto</a-select-option>
|
||||
<a-select-option v-for="[label, val] in CIPHER_SUITES" :key="val" :value="val">{{ label
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model:value="inbound.stream.tls.minVersion" :style="{ width: '50%' }">
|
||||
<a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="inbound.stream.tls.maxVersion" :style="{ width: '50%' }">
|
||||
<a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model:value="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }">
|
||||
<a-select-option value="">None</a-select-option>
|
||||
<a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select v-model:value="inbound.stream.tls.alpn" mode="multiple" :style="{ width: '100%' }"
|
||||
:token-separators="[',']">
|
||||
<a-select-option v-for="a in ALPNS" :key="a" :value="a">{{ a }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Reject Unknown SNI">
|
||||
<a-switch v-model:checked="inbound.stream.tls.rejectUnknownSni" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Disable System Root">
|
||||
<a-switch v-model:checked="inbound.stream.tls.disableSystemRoot" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Resumption">
|
||||
<a-switch v-model:checked="inbound.stream.tls.enableSessionResumption" />
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<!-- Cert array — file path or inline content per row -->
|
||||
<template v-for="(cert, idx) in inbound.stream.tls.certs" :key="`cert-${idx}`">
|
||||
<a-form-item :label="t('certificate')">
|
||||
<a-radio-group v-model:value="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ t('pages.inbounds.certificatePath') }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ t('pages.inbounds.certificateContent') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button v-if="idx === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button v-if="inbound.stream.tls.certs.length > 1" type="primary" size="small"
|
||||
@click="inbound.stream.tls.removeCert(idx)">
|
||||
<template #icon>
|
||||
<MinusOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||
<a-input v-model:value="cert.certFile" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.privatekey')">
|
||||
<a-input v-model:value="cert.keyFile" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" :disabled="!defaultCert && !defaultKey" @click="setDefaultCertData(idx)">
|
||||
{{ t('pages.inbounds.setDefaultCert') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||
<a-textarea v-model:value="cert.cert" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.privatekey')">
|
||||
<a-textarea v-model:value="cert.key" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="One Time Loading">
|
||||
<a-switch v-model:checked="cert.oneTimeLoading" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Usage Option">
|
||||
<a-select v-model:value="cert.usage" :style="{ width: '50%' }">
|
||||
<a-select-option v-for="u in USAGES" :key="u" :value="u">{{ u }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="cert.usage === 'issue'" label="Build Chain">
|
||||
<a-switch v-model:checked="cert.buildChain" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- ECH (Encrypted Client Hello) -->
|
||||
<a-form-item label="ECH key">
|
||||
<a-input v-model:value="inbound.stream.tls.echServerKeys" />
|
||||
</a-form-item>
|
||||
<a-form-item label="ECH config">
|
||||
<a-input v-model:value="inbound.stream.tls.settings.echConfigList" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="saving" @click="getNewEchCert">Get New ECH Cert</a-button>
|
||||
<a-button danger @click="clearEchCert">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="security === 'reality' && inbound.stream.reality">
|
||||
<a-form-item label="Show">
|
||||
<a-switch v-model:checked="inbound.stream.reality.show" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Xver">
|
||||
<a-input-number v-model:value="inbound.stream.reality.xver" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model:value="inbound.stream.reality.settings.fingerprint" :style="{ width: '100%' }">
|
||||
<a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
Target
|
||||
<SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
|
||||
</template>
|
||||
<a-input v-model:value="inbound.stream.reality.target" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
SNI
|
||||
<SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
|
||||
</template>
|
||||
<a-input v-model:value="inbound.stream.reality.serverNames" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Time Diff (ms)">
|
||||
<a-input-number v-model:value="inbound.stream.reality.maxTimediff" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Min Client Ver">
|
||||
<a-input v-model:value="inbound.stream.reality.minClientVer" placeholder="25.9.11" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Client Ver">
|
||||
<a-input v-model:value="inbound.stream.reality.maxClientVer" placeholder="25.9.11" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
Short IDs
|
||||
<SyncOutlined class="random-icon" @click="randomizeShortIds" />
|
||||
</template>
|
||||
<a-textarea v-model:value="inbound.stream.reality.shortIds" :auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label="SpiderX">
|
||||
<a-input v-model:value="inbound.stream.reality.settings.spiderX" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||
<a-textarea v-model:value="inbound.stream.reality.settings.publicKey"
|
||||
:auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.privatekey')">
|
||||
<a-textarea v-model:value="inbound.stream.reality.privateKey" :auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="saving" @click="genRealityKeypair">Get New Cert</a-button>
|
||||
<a-button danger @click="clearRealityKeypair">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Seed">
|
||||
<a-textarea v-model:value="inbound.stream.reality.mldsa65Seed" :auto-size="{ minRows: 2, maxRows: 6 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Verify">
|
||||
<a-textarea v-model:value="inbound.stream.reality.settings.mldsa65Verify"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="saving" @click="genMldsa65">Get New Seed</a-button>
|
||||
<a-button danger @click="clearMldsa65">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- ====== External Proxy ====== -->
|
||||
<a-form-item label="External Proxy">
|
||||
<a-switch v-model:checked="externalProxy" />
|
||||
|
|
@ -2164,6 +1811,205 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
|
|||
<FinalMaskForm :stream="inbound.stream" :protocol="protocol" />
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- ============================== SECURITY ============================== -->
|
||||
<a-tab-pane v-if="canEnableStream" key="security" tab="Security">
|
||||
<a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
|
||||
<a-form-item label="Security">
|
||||
<a-select v-model:value="security" :style="{ width: '160px' }" :disabled="!canEnableTls">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="tls">tls</a-select-option>
|
||||
<a-select-option v-if="canEnableReality" value="reality">reality</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="security === 'tls' && inbound.stream.tls">
|
||||
<a-form-item label="SNI">
|
||||
<a-input v-model:value="inbound.stream.tls.sni" placeholder="Server Name Indication" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Cipher Suites">
|
||||
<a-select v-model:value="inbound.stream.tls.cipherSuites">
|
||||
<a-select-option value="">Auto</a-select-option>
|
||||
<a-select-option v-for="[label, val] in CIPHER_SUITES" :key="val" :value="val">{{ label
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model:value="inbound.stream.tls.minVersion" :style="{ width: '50%' }">
|
||||
<a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="inbound.stream.tls.maxVersion" :style="{ width: '50%' }">
|
||||
<a-select-option v-for="v in TLS_VERSIONS" :key="v" :value="v">{{ v }}</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model:value="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }">
|
||||
<a-select-option value="">None</a-select-option>
|
||||
<a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select v-model:value="inbound.stream.tls.alpn" mode="multiple" :style="{ width: '100%' }"
|
||||
:token-separators="[',']">
|
||||
<a-select-option v-for="a in ALPNS" :key="a" :value="a">{{ a }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Reject Unknown SNI">
|
||||
<a-switch v-model:checked="inbound.stream.tls.rejectUnknownSni" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Disable System Root">
|
||||
<a-switch v-model:checked="inbound.stream.tls.disableSystemRoot" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Resumption">
|
||||
<a-switch v-model:checked="inbound.stream.tls.enableSessionResumption" />
|
||||
</a-form-item>
|
||||
|
||||
<template v-for="(cert, idx) in inbound.stream.tls.certs" :key="`cert-${idx}`">
|
||||
<a-form-item :label="t('certificate')">
|
||||
<a-radio-group v-model:value="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ t('pages.inbounds.certificatePath') }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ t('pages.inbounds.certificateContent') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button v-if="idx === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button v-if="inbound.stream.tls.certs.length > 1" type="primary" size="small"
|
||||
@click="inbound.stream.tls.removeCert(idx)">
|
||||
<template #icon>
|
||||
<MinusOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||
<a-input v-model:value="cert.certFile" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.privatekey')">
|
||||
<a-input v-model:value="cert.keyFile" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" :disabled="!defaultCert && !defaultKey" @click="setDefaultCertData(idx)">
|
||||
{{ t('pages.inbounds.setDefaultCert') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||
<a-textarea v-model:value="cert.cert" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.privatekey')">
|
||||
<a-textarea v-model:value="cert.key" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="One Time Loading">
|
||||
<a-switch v-model:checked="cert.oneTimeLoading" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Usage Option">
|
||||
<a-select v-model:value="cert.usage" :style="{ width: '50%' }">
|
||||
<a-select-option v-for="u in USAGES" :key="u" :value="u">{{ u }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="cert.usage === 'issue'" label="Build Chain">
|
||||
<a-switch v-model:checked="cert.buildChain" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item label="ECH key">
|
||||
<a-input v-model:value="inbound.stream.tls.echServerKeys" />
|
||||
</a-form-item>
|
||||
<a-form-item label="ECH config">
|
||||
<a-input v-model:value="inbound.stream.tls.settings.echConfigList" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="saving" @click="getNewEchCert">Get New ECH Cert</a-button>
|
||||
<a-button danger @click="clearEchCert">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="security === 'reality' && inbound.stream.reality">
|
||||
<a-form-item label="Show">
|
||||
<a-switch v-model:checked="inbound.stream.reality.show" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Xver">
|
||||
<a-input-number v-model:value="inbound.stream.reality.xver" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model:value="inbound.stream.reality.settings.fingerprint" :style="{ width: '100%' }">
|
||||
<a-select-option v-for="fp in FINGERPRINTS" :key="fp" :value="fp">{{ fp }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
Target
|
||||
<SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
|
||||
</template>
|
||||
<a-input v-model:value="inbound.stream.reality.target" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
SNI
|
||||
<SyncOutlined class="random-icon" @click="randomizeRealityTarget" />
|
||||
</template>
|
||||
<a-input v-model:value="inbound.stream.reality.serverNames" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Time Diff (ms)">
|
||||
<a-input-number v-model:value="inbound.stream.reality.maxTimediff" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Min Client Ver">
|
||||
<a-input v-model:value="inbound.stream.reality.minClientVer" placeholder="25.9.11" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Client Ver">
|
||||
<a-input v-model:value="inbound.stream.reality.maxClientVer" placeholder="25.9.11" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
Short IDs
|
||||
<SyncOutlined class="random-icon" @click="randomizeShortIds" />
|
||||
</template>
|
||||
<a-textarea v-model:value="inbound.stream.reality.shortIds" :auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label="SpiderX">
|
||||
<a-input v-model:value="inbound.stream.reality.settings.spiderX" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||
<a-textarea v-model:value="inbound.stream.reality.settings.publicKey"
|
||||
:auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('pages.inbounds.privatekey')">
|
||||
<a-textarea v-model:value="inbound.stream.reality.privateKey" :auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="saving" @click="genRealityKeypair">Get New Cert</a-button>
|
||||
<a-button danger @click="clearRealityKeypair">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Seed">
|
||||
<a-textarea v-model:value="inbound.stream.reality.mldsa65Seed" :auto-size="{ minRows: 2, maxRows: 6 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Verify">
|
||||
<a-textarea v-model:value="inbound.stream.reality.settings.mldsa65Verify"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="saving" @click="genMldsa65">Get New Seed</a-button>
|
||||
<a-button danger @click="clearMldsa65">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- ============================== SNIFFING ============================== -->
|
||||
<a-tab-pane key="sniffing" tab="Sniffing"><!-- "Sniffing" stays literal — xray config term -->
|
||||
<a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
|
||||
|
|
@ -2285,18 +2131,6 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
|
|||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.client-summary {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.client-summary th,
|
||||
.client-summary td {
|
||||
padding: 4px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.15);
|
||||
}
|
||||
|
||||
.fallbacks-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Reference in a new issue