| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417 |
- import { useEffect, useRef, useState } from 'react';
- import { useTranslation } from 'react-i18next';
- import dayjs from 'dayjs';
- import {
- Button,
- Card,
- Checkbox,
- Empty,
- Form,
- Input,
- InputNumber,
- Modal,
- Radio,
- Select,
- Space,
- Switch,
- Tabs,
- Tooltip,
- Typography,
- message,
- } from 'antd';
- import {
- ArrowDownOutlined,
- ArrowUpOutlined,
- DeleteOutlined,
- MinusOutlined,
- PlusOutlined,
- SyncOutlined,
- } from '@ant-design/icons';
- import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
- import {
- rawInboundToFormValues,
- formValuesToWirePayload,
- } from '@/lib/xray/inbound-form-adapter';
- import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
- import {
- canEnableReality,
- canEnableStream,
- canEnableTls,
- isSS2022,
- } from '@/lib/xray/protocol-capabilities';
- import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
- import { getRandomRealityTarget } from '@/models/reality-targets';
- import {
- InboundFormBaseSchema,
- InboundFormSchema,
- type FallbackRow,
- type InboundFormValues,
- } from '@/schemas/forms/inbound-form';
- import { antdRule } from '@/utils/zodForm';
- import {
- ALPN_OPTION,
- DOMAIN_STRATEGY_OPTION,
- Protocols,
- SNIFFING_OPTION,
- TCP_CONGESTION_OPTION,
- TLS_CIPHER_OPTION,
- TLS_VERSION_OPTION,
- USAGE_OPTION,
- UTLS_FINGERPRINT,
- } from '@/schemas/primitives';
- import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
- import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
- import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
- import DateTimePicker from '@/components/DateTimePicker';
- import HeaderMapEditor from '@/components/HeaderMapEditor';
- import InputAddon from '@/components/InputAddon';
- import JsonEditor from '@/components/JsonEditor';
- import type { FormInstance } from 'antd';
- import type { NamePath } from 'antd/es/form/interface';
- const { TextArea } = Input;
- import type { DBInbound } from '@/models/dbinbound';
- import type { NodeRecord } from '@/api/queries/useNodesQuery';
- // Pattern A rewrite of InboundFormModal. Built as a sibling file so the
- // build stays green while the rewrite progresses section by section.
- // InboundsPage continues to render the old InboundFormModal.tsx until the
- // atomic swap at the end (Core Decision 7).
- const { Text } = Typography;
- // Sub-editor for one slice of the form (settings, streamSettings, sniffing).
- // Holds a local text buffer so the user can type freely; on every keystroke
- // we try to JSON.parse and forward the result to form state. Invalid JSON
- // is held in the buffer until the next valid moment — no panic on partial
- // input. The buffer seeds once on mount; the modal's destroyOnHidden makes
- // each open a fresh editor instance, so we don't need to re-sync on outer
- // form changes.
- function AdvancedSliceEditor({
- form,
- path,
- minHeight,
- maxHeight,
- }: {
- form: FormInstance<InboundFormValues>;
- path: NamePath;
- minHeight?: string;
- maxHeight?: string;
- }) {
- const [text, setText] = useState(() =>
- JSON.stringify(form.getFieldValue(path) ?? {}, null, 2),
- );
- return (
- <JsonEditor
- value={text}
- minHeight={minHeight}
- maxHeight={maxHeight}
- onChange={(next) => {
- setText(next);
- try {
- form.setFieldValue(path, JSON.parse(next));
- } catch {
- }
- }}
- />
- );
- }
- const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
- const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
- const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
- Protocols.VLESS,
- Protocols.VMESS,
- Protocols.TROJAN,
- Protocols.SHADOWSOCKS,
- Protocols.HYSTERIA,
- Protocols.WIREGUARD,
- ]);
- interface InboundFormModalProps {
- open: boolean;
- onClose: () => void;
- onSaved: () => void;
- mode: 'add' | 'edit';
- dbInbound: DBInbound | null;
- dbInbounds: DBInbound[];
- availableNodes?: NodeRecord[];
- }
- function buildAddModeValues(): InboundFormValues {
- const settings = createDefaultInboundSettings('vless') ?? undefined;
- return rawInboundToFormValues({
- protocol: 'vless',
- settings,
- streamSettings: { network: 'tcp', security: 'none' },
- sniffing: {},
- port: RandomUtil.randomInteger(10000, 60000),
- listen: '',
- tag: '',
- enable: true,
- trafficReset: 'never',
- });
- }
- export default function InboundFormModal({
- open,
- onClose,
- onSaved,
- mode,
- dbInbound,
- dbInbounds,
- availableNodes,
- }: InboundFormModalProps) {
- const { t } = useTranslation();
- const [messageApi, messageContextHolder] = message.useMessage();
- const [form] = Form.useForm<InboundFormValues>();
- const [saving, setSaving] = useState(false);
- const fallbackKeyRef = useRef(0);
- const [fallbacks, setFallbacks] = useState<FallbackRow[]>([]);
- const selectableNodes = (availableNodes || []).filter((n) => n.enable);
- const protocol = (Form.useWatch('protocol', form) ?? '') as string;
- const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(protocol);
- const sniffingEnabled = Form.useWatch(['sniffing', 'enabled'], form) ?? false;
- const vlessEncryption = Form.useWatch(['settings', 'encryption'], form) ?? '';
- const ssMethod = Form.useWatch(['settings', 'method'], form);
- const isSSWith2022 = isSS2022({
- protocol,
- settings: typeof ssMethod === 'string' ? { method: ssMethod } : {},
- });
- const mixedUdpOn = Form.useWatch(['settings', 'udp'], form) ?? false;
- const network = Form.useWatch(['streamSettings', 'network'], form) ?? '';
- const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none';
- const streamEnabled = canEnableStream({ protocol });
- const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
- const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
- const isFallbackHost =
- (protocol === Protocols.VLESS || protocol === Protocols.TROJAN)
- && network === 'tcp'
- && (security === 'tls' || security === 'reality');
- const fallbackChildOptions = (dbInbounds || [])
- .filter((ib) => ib.id !== dbInbound?.id)
- .map((ib) => ({
- label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
- value: ib.id,
- }));
- const loadFallbacks = async (masterId: number | null) => {
- if (!masterId) {
- setFallbacks([]);
- return;
- }
- const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`);
- if (!msg?.success || !Array.isArray(msg.obj)) {
- setFallbacks([]);
- return;
- }
- setFallbacks(
- (msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[])
- .map((r) => ({
- rowKey: `fb-${++fallbackKeyRef.current}`,
- childId: r.childId,
- name: r.name || '',
- alpn: r.alpn || '',
- path: r.path || '',
- xver: r.xver || 0,
- })),
- );
- };
- const saveFallbacks = async (masterId: number) => {
- if (!masterId) return true;
- const payload = {
- fallbacks: fallbacks.filter((c) => c.childId).map((c, i) => ({
- childId: c.childId,
- name: c.name,
- alpn: c.alpn,
- path: c.path,
- xver: Number(c.xver) || 0,
- sortOrder: i,
- })),
- };
- const msg = await HttpUtil.post(
- `/panel/api/inbounds/${masterId}/fallbacks`,
- payload,
- { headers: { 'Content-Type': 'application/json' } },
- );
- return !!msg?.success;
- };
- const addFallback = () => {
- setFallbacks((prev) => [...prev, {
- rowKey: `fb-${++fallbackKeyRef.current}`,
- childId: null,
- name: '',
- alpn: '',
- path: '',
- xver: 0,
- }]);
- };
- const updateFallback = (rowKey: string, patch: Partial<FallbackRow>) => {
- setFallbacks((prev) => prev.map((r) => r.rowKey === rowKey ? { ...r, ...patch } : r));
- };
- const removeFallback = (idx: number) => {
- setFallbacks((prev) => prev.filter((_, i) => i !== idx));
- };
- // Move a fallback row up/down by swapping adjacent indices. The order
- // is persisted via the fallback row's sortOrder (rebuilt by index on
- // save), so reordering survives reloads.
- const moveFallback = (idx: number, direction: -1 | 1) => {
- setFallbacks((prev) => {
- const target = idx + direction;
- if (target < 0 || target >= prev.length) return prev;
- const next = prev.slice();
- [next[idx], next[target]] = [next[target], next[idx]];
- return next;
- });
- };
- // One-shot: add a fresh fallback row for every eligible inbound (i.e.
- // every option in fallbackChildOptions) that is not already wired up.
- // Convenient for operators who want catch-all routing to every host
- // they manage on the panel.
- const addAllFallbacks = () => {
- setFallbacks((prev) => {
- const alreadyHave = new Set(prev.map((r) => r.childId));
- const additions = fallbackChildOptions
- .filter((opt) => !alreadyHave.has(opt.value))
- .map<FallbackRow>((opt) => ({
- rowKey: `fb-${++fallbackKeyRef.current}`,
- childId: opt.value,
- name: '',
- alpn: '',
- path: '',
- xver: 0,
- }));
- if (additions.length === 0) return prev;
- return [...prev, ...additions];
- });
- };
- const genRealityKeypair = async () => {
- setSaving(true);
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
- if (msg?.success) {
- const obj = msg.obj as { privateKey: string; publicKey: string };
- form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey);
- form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey);
- }
- } finally {
- setSaving(false);
- }
- };
- const clearRealityKeypair = () => {
- form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], '');
- form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], '');
- };
- const genMldsa65 = async () => {
- setSaving(true);
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
- if (msg?.success) {
- const obj = msg.obj as { seed: string; verify: string };
- form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], obj.seed);
- form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], obj.verify);
- }
- } finally {
- setSaving(false);
- }
- };
- const clearMldsa65 = () => {
- form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], '');
- form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], '');
- };
- const randomizeRealityTarget = () => {
- const tgt = getRandomRealityTarget() as { target: string; sni: string };
- form.setFieldValue(['streamSettings', 'realitySettings', 'target'], tgt.target);
- form.setFieldValue(
- ['streamSettings', 'realitySettings', 'serverNames'],
- tgt.sni.split(',').map((s) => s.trim()).filter(Boolean),
- );
- };
- const randomizeShortIds = () => {
- form.setFieldValue(
- ['streamSettings', 'realitySettings', 'shortIds'],
- RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean),
- );
- };
- const getNewEchCert = async () => {
- const sni = form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']);
- setSaving(true);
- try {
- const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni });
- if (msg?.success) {
- const obj = msg.obj as { echServerKeys: string; echConfigList: string };
- form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], obj.echServerKeys);
- form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], obj.echConfigList);
- }
- } finally {
- setSaving(false);
- }
- };
- const clearEchCert = () => {
- form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], '');
- form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
- };
- const onSecurityChange = (next: string) => {
- const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
- const cleaned: Record<string, unknown> = { ...current, security: next };
- delete cleaned.tlsSettings;
- delete cleaned.realitySettings;
- if (next === 'tls') cleaned.tlsSettings = TlsStreamSettingsSchema.parse({});
- if (next === 'reality') cleaned.realitySettings = RealityStreamSettingsSchema.parse({});
- form.setFieldValue('streamSettings', cleaned);
- };
- const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form);
- const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false;
- const xhttpSessionPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionPlacement'], form);
- const xhttpSeqPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'seqPlacement'], form);
- const xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form);
- const externalProxyArr = Form.useWatch(['streamSettings', 'externalProxy'], form);
- const externalProxyOn = Array.isArray(externalProxyArr) && externalProxyArr.length > 0;
- const sockoptValue = Form.useWatch(['streamSettings', 'sockopt'], form);
- const sockoptOn = !!sockoptValue && typeof sockoptValue === 'object' && Object.keys(sockoptValue as object).length > 0;
- const toggleExternalProxy = (on: boolean) => {
- if (on) {
- const port = (form.getFieldValue('port') as number) ?? 443;
- form.setFieldValue(['streamSettings', 'externalProxy'], [{
- forceTls: 'same',
- dest: typeof window !== 'undefined' ? window.location.hostname : '',
- port,
- remark: '',
- sni: '',
- fingerprint: '',
- alpn: [],
- }]);
- } else {
- form.setFieldValue(['streamSettings', 'externalProxy'], []);
- }
- };
- const toggleSockopt = (on: boolean) => {
- if (on) {
- form.setFieldValue(
- ['streamSettings', 'sockopt'],
- SockoptStreamSettingsSchema.parse({}),
- );
- } else {
- form.setFieldValue(['streamSettings', 'sockopt'], undefined);
- }
- };
- const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form);
- const wgPubKey = typeof wgSecretKey === 'string' && wgSecretKey.length > 0
- ? Wireguard.generateKeypair(wgSecretKey).publicKey
- : '';
- const regenInboundWg = () => {
- const kp = Wireguard.generateKeypair();
- form.setFieldValue(['settings', 'secretKey'], kp.privateKey);
- };
- const regenWgPeerKeypair = (peerName: number) => {
- const kp = Wireguard.generateKeypair();
- form.setFieldValue(['settings', 'peers', peerName, 'privateKey'], kp.privateKey);
- form.setFieldValue(['settings', 'peers', peerName, 'publicKey'], kp.publicKey);
- };
- const matchesVlessAuth = (
- block: { id?: string; label?: string } | undefined | null,
- authId: string,
- ) => {
- if (block?.id === authId) return true;
- const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, '');
- if (authId === 'mlkem768') return label.includes('mlkem768');
- if (authId === 'x25519') return label.includes('x25519');
- return false;
- };
- const getNewVlessEnc = async (authId: string) => {
- if (!authId) return;
- setSaving(true);
- try {
- const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
- if (!msg?.success) return;
- const obj = msg.obj as {
- auths?: { decryption: string; encryption: string; label?: string; id?: string }[];
- };
- const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId));
- if (!block) return;
- form.setFieldValue(['settings', 'decryption'], block.decryption);
- form.setFieldValue(['settings', 'encryption'], block.encryption);
- } finally {
- setSaving(false);
- }
- };
- const clearVlessEnc = () => {
- form.setFieldValue(['settings', 'decryption'], 'none');
- form.setFieldValue(['settings', 'encryption'], 'none');
- };
- const selectedVlessAuth = (() => {
- const enc = typeof vlessEncryption === 'string' ? vlessEncryption : '';
- if (!enc || enc === 'none') return 'None';
- const parts = enc.split('.').filter(Boolean);
- const authKey = parts[parts.length - 1] || '';
- if (!authKey) return t('pages.inbounds.vlessAuthCustom');
- return authKey.length > 300
- ? t('pages.inbounds.vlessAuthMlkem768')
- : t('pages.inbounds.vlessAuthX25519');
- })();
- useEffect(() => {
- if (!open) return;
- const initial = mode === 'edit' && dbInbound
- ? rawInboundToFormValues(dbInbound)
- : buildAddModeValues();
- form.resetFields();
- form.setFieldsValue(initial);
- if (
- mode === 'edit'
- && dbInbound
- && (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN)
- ) {
- loadFallbacks(dbInbound.id);
- } else {
- setFallbacks([]);
- }
- }, [open, mode, dbInbound, form]);
- // Why: protocol picker reset cascades through the form — clearing the
- // settings DU branch and dropping a nodeId that no longer applies. The
- // legacy modal did this imperatively in onProtocolChange; here we hook
- // into AntD's onValuesChange and let setFieldValue keep the rest of
- // the form state intact.
- const onValuesChange = (changed: Partial<InboundFormValues>) => {
- if (mode === 'edit') return;
- if ('protocol' in changed && typeof changed.protocol === 'string') {
- const next = changed.protocol;
- const settings = createDefaultInboundSettings(next) ?? undefined;
- form.setFieldValue('settings', settings);
- if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) {
- form.setFieldValue('nodeId', null);
- }
- }
- };
- const submit = async () => {
- let values: InboundFormValues;
- try {
- values = await form.validateFields();
- } catch {
- return;
- }
- const parsed = InboundFormSchema.safeParse(values);
- if (!parsed.success) {
- const issue = parsed.error.issues[0];
- messageApi.error(
- t(issue?.message ?? 'somethingWentWrong', {
- defaultValue: issue?.message ?? 'invalid',
- }),
- );
- return;
- }
- setSaving(true);
- try {
- const payload = formValuesToWirePayload(parsed.data);
- const url = mode === 'edit' && dbInbound
- ? `/panel/api/inbounds/update/${dbInbound.id}`
- : '/panel/api/inbounds/add';
- const msg = await HttpUtil.post(url, payload);
- if (msg?.success) {
- if (isFallbackHost) {
- const obj = msg.obj as { id?: number; Id?: number } | null;
- const masterId = mode === 'edit'
- ? dbInbound!.id
- : (obj?.id ?? obj?.Id ?? 0);
- if (masterId) await saveFallbacks(masterId);
- }
- onSaved();
- onClose();
- }
- } finally {
- setSaving(false);
- }
- };
- const title = mode === 'edit'
- ? t('pages.inbounds.modifyInbound')
- : t('pages.inbounds.addInbound');
- const okText = mode === 'edit'
- ? t('pages.clients.submitEdit')
- : t('create');
- const basicTab = (
- <>
- <Form.Item name="enable" label={t('enable')} valuePropName="checked">
- <Switch />
- </Form.Item>
- <Form.Item name="remark" label={t('pages.inbounds.remark')}>
- <Input />
- </Form.Item>
- {selectableNodes.length > 0 && isNodeEligible && (
- <Form.Item name="nodeId" label={t('pages.inbounds.deployTo')}>
- <Select
- disabled={mode === 'edit'}
- placeholder={t('pages.inbounds.localPanel')}
- allowClear
- >
- <Select.Option value={null}>{t('pages.inbounds.localPanel')}</Select.Option>
- {selectableNodes.map((n) => (
- <Select.Option
- key={n.id}
- value={n.id}
- disabled={n.status === 'offline'}
- >
- {n.name}{n.status === 'offline' ? ' (offline)' : ''}
- </Select.Option>
- ))}
- </Select>
- </Form.Item>
- )}
- <Form.Item name="protocol" label={t('pages.inbounds.protocol')}>
- <Select disabled={mode === 'edit'} options={PROTOCOL_OPTIONS} />
- </Form.Item>
- <Form.Item name="listen" label={t('pages.inbounds.address')}>
- <Input placeholder={t('pages.inbounds.monitorDesc')} />
- </Form.Item>
- <Form.Item
- name="port"
- label={t('pages.inbounds.port')}
- rules={[antdRule(InboundFormBaseSchema.shape.port, t)]}
- >
- <InputNumber min={1} max={65535} />
- </Form.Item>
- <Form.Item
- label={
- <Tooltip title={t('pages.inbounds.meansNoLimit')}>
- {t('pages.inbounds.totalFlow')}
- </Tooltip>
- }
- >
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) => prev.total !== curr.total}
- >
- {({ getFieldValue, setFieldValue }) => {
- const totalBytes = (getFieldValue('total') as number) ?? 0;
- const totalGB = totalBytes
- ? Math.round((totalBytes / SizeFormatter.ONE_GB) * 100) / 100
- : 0;
- return (
- <InputNumber
- value={totalGB}
- min={0}
- step={1}
- onChange={(v) => {
- const bytes = NumberFormatter.toFixed(
- (Number(v) || 0) * SizeFormatter.ONE_GB,
- 0,
- );
- setFieldValue('total', bytes);
- }}
- />
- );
- }}
- </Form.Item>
- </Form.Item>
- <Form.Item name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
- <Select>
- {TRAFFIC_RESETS.map((r) => (
- <Select.Option key={r} value={r}>
- {t(`pages.inbounds.periodicTrafficReset.${r}`)}
- </Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- label={
- <Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
- {t('pages.inbounds.expireDate')}
- </Tooltip>
- }
- >
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) => prev.expiryTime !== curr.expiryTime}
- >
- {({ getFieldValue, setFieldValue }) => {
- const expiry = (getFieldValue('expiryTime') as number) ?? 0;
- return (
- <DateTimePicker
- value={expiry > 0 ? dayjs(expiry) : null}
- onChange={(d) => setFieldValue('expiryTime', d ? d.valueOf() : 0)}
- />
- );
- }}
- </Form.Item>
- </Form.Item>
- </>
- );
- const fallbacksCard = (
- <Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
- {fallbacks.length === 0 && (
- <Empty
- description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'}
- styles={{ image: { height: 40 } }}
- style={{ margin: '8px 0 12px' }}
- />
- )}
- {fallbacks.map((record, idx) => (
- <div
- key={record.rowKey}
- style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}
- >
- <Space.Compact block style={{ marginBottom: 6 }}>
- <Select
- value={record.childId}
- options={fallbackChildOptions}
- showSearch
- placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
- filterOption={(input, option) =>
- ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())
- }
- style={{ width: '100%' }}
- onChange={(v) => updateFallback(record.rowKey, { childId: v })}
- />
- <Button
- disabled={idx === 0}
- onClick={() => moveFallback(idx, -1)}
- title="Move up"
- >
- <ArrowUpOutlined />
- </Button>
- <Button
- disabled={idx === fallbacks.length - 1}
- onClick={() => moveFallback(idx, 1)}
- title="Move down"
- >
- <ArrowDownOutlined />
- </Button>
- <Button danger onClick={() => removeFallback(idx)}>
- <DeleteOutlined />
- </Button>
- </Space.Compact>
- <Space.Compact block>
- <InputAddon>SNI</InputAddon>
- <Input
- placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
- value={record.name}
- onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
- />
- <InputAddon>ALPN</InputAddon>
- <Input
- placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
- value={record.alpn}
- onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
- />
- <InputAddon>Path</InputAddon>
- <Input
- placeholder="/"
- value={record.path}
- onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
- />
- <InputAddon>xver</InputAddon>
- <InputNumber
- min={0}
- max={2}
- value={record.xver}
- onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
- />
- </Space.Compact>
- </div>
- ))}
- <Space>
- <Button size="small" onClick={addFallback}>
- <PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
- </Button>
- <Button
- size="small"
- onClick={addAllFallbacks}
- disabled={fallbackChildOptions.length === 0
- || fallbacks.length >= fallbackChildOptions.length}
- title="Add a fallback row for every eligible inbound not yet wired up"
- >
- Add all
- </Button>
- </Space>
- </Card>
- );
- const protocolTab = (
- <>
- {protocol === Protocols.WIREGUARD && (
- <>
- <Form.Item
- name={['settings', 'secretKey']}
- label={
- <>
- Secret key{' '}
- <SyncOutlined className="random-icon" onClick={regenInboundWg} />
- </>
- }
- >
- <Input />
- </Form.Item>
- <Form.Item label="Public key">
- <Input value={wgPubKey} disabled />
- </Form.Item>
- <Form.Item name={['settings', 'mtu']} label="MTU">
- <InputNumber />
- </Form.Item>
- <Form.Item
- name={['settings', 'noKernelTun']}
- label="No-kernel TUN"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.List name={['settings', 'peers']}>
- {(fields, { add, remove }) => (
- <>
- <Form.Item label="Peers">
- <Button
- size="small"
- onClick={() => add({
- publicKey: '',
- allowedIPs: [],
- })}
- >
- <PlusOutlined /> Add peer
- </Button>
- </Form.Item>
- {fields.map((field, idx) => (
- <div key={field.key} className="wg-peer">
- <Form.Item label={`Peer ${idx + 1}`}>
- {fields.length > 1 && (
- <Button
- size="small"
- danger
- onClick={() => remove(field.name)}
- >
- <MinusOutlined />
- </Button>
- )}
- </Form.Item>
- <Form.Item
- name={[field.name, 'privateKey']}
- label={
- <>
- Secret key{' '}
- <SyncOutlined
- className="random-icon"
- onClick={() => regenWgPeerKeypair(field.name)}
- />
- </>
- }
- >
- <Input />
- </Form.Item>
- <Form.Item name={[field.name, 'publicKey']} label="Public key">
- <Input />
- </Form.Item>
- <Form.Item name={[field.name, 'preSharedKey']} label="PSK">
- <Input />
- </Form.Item>
- <Form.List name={[field.name, 'allowedIPs']}>
- {(ipFields, { add: addIp, remove: removeIp }) => (
- <Form.Item label="Allowed IPs">
- <Button size="small" onClick={() => addIp('')}>
- <PlusOutlined />
- </Button>
- {ipFields.map((ipField) => (
- <Space.Compact key={ipField.key} block className="mt-4">
- <Form.Item name={ipField.name} noStyle>
- <Input />
- </Form.Item>
- {ipFields.length > 1 && (
- <Button size="small" onClick={() => removeIp(ipField.name)}>
- <MinusOutlined />
- </Button>
- )}
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </Form.List>
- <Form.Item name={[field.name, 'keepAlive']} label="Keep-alive">
- <InputNumber min={0} />
- </Form.Item>
- </div>
- ))}
- </>
- )}
- </Form.List>
- </>
- )}
- {protocol === Protocols.TUN && (
- <>
- <Form.Item name={['settings', 'name']} label="Interface name">
- <Input placeholder="xray0" />
- </Form.Item>
- <Form.Item name={['settings', 'mtu']} label="MTU">
- <InputNumber min={0} />
- </Form.Item>
- <Form.List name={['settings', 'gateway']}>
- {(fields, { add, remove }) => (
- <Form.Item label="Gateway">
- <Button size="small" onClick={() => add('')}>
- <PlusOutlined />
- </Button>
- {fields.map((field, j) => (
- <Space.Compact key={field.key} block className="mt-4">
- <Form.Item name={field.name} noStyle>
- <Input placeholder={j === 0 ? '10.0.0.1/16' : 'fc00::1/64'} />
- </Form.Item>
- <Button size="small" onClick={() => remove(field.name)}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </Form.List>
- <Form.List name={['settings', 'dns']}>
- {(fields, { add, remove }) => (
- <Form.Item label="DNS">
- <Button size="small" onClick={() => add('')}>
- <PlusOutlined />
- </Button>
- {fields.map((field, j) => (
- <Space.Compact key={field.key} block className="mt-4">
- <Form.Item name={field.name} noStyle>
- <Input placeholder={j === 0 ? '1.1.1.1' : '8.8.8.8'} />
- </Form.Item>
- <Button size="small" onClick={() => remove(field.name)}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </Form.List>
- <Form.Item name={['settings', 'userLevel']} label="User level">
- <InputNumber min={0} />
- </Form.Item>
- <Form.List name={['settings', 'autoSystemRoutingTable']}>
- {(fields, { add, remove }) => (
- <Form.Item
- label={
- <Tooltip title="Windows-only. CIDRs added to the system routing table automatically so matching traffic goes through TUN.">
- Auto system routes
- </Tooltip>
- }
- >
- <Button size="small" onClick={() => add('')}>
- <PlusOutlined />
- </Button>
- {fields.map((field, j) => (
- <Space.Compact key={field.key} block className="mt-4">
- <Form.Item name={field.name} noStyle>
- <Input placeholder={j === 0 ? '0.0.0.0/0' : '::/0'} />
- </Form.Item>
- <Button size="small" onClick={() => remove(field.name)}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </Form.List>
- <Form.Item
- name={['settings', 'autoOutboundsInterface']}
- label={
- <Tooltip title="Physical interface for outbound traffic. Use 'auto' to detect; auto-enabled when Auto system routes is set.">
- Auto outbounds interface
- </Tooltip>
- }
- >
- <Input placeholder="auto" />
- </Form.Item>
- </>
- )}
- {protocol === Protocols.TUNNEL && (
- <>
- <Form.Item name={['settings', 'rewriteAddress']} label="Rewrite address">
- <Input />
- </Form.Item>
- <Form.Item name={['settings', 'rewritePort']} label="Rewrite port">
- <InputNumber min={0} max={65535} />
- </Form.Item>
- <Form.Item name={['settings', 'allowedNetwork']} label="Allowed network">
- <Select>
- <Select.Option value="tcp,udp">TCP, UDP</Select.Option>
- <Select.Option value="tcp">TCP</Select.Option>
- <Select.Option value="udp">UDP</Select.Option>
- </Select>
- </Form.Item>
- <Form.List name={['settings', 'portMap']}>
- {(fields, { add, remove }) => (
- <>
- <Form.Item label="Port map">
- <Button size="small" onClick={() => add({ name: '', value: '' })}>
- <PlusOutlined />
- </Button>
- </Form.Item>
- {fields.length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {fields.map((field, idx) => (
- <Space.Compact key={field.key} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Form.Item name={[field.name, 'name']} noStyle>
- <Input placeholder="5555" />
- </Form.Item>
- <Form.Item name={[field.name, 'value']} noStyle>
- <Input placeholder="1.1.1.1:7777" />
- </Form.Item>
- <Button onClick={() => remove(field.name)}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </>
- )}
- </Form.List>
- <Form.Item
- name={['settings', 'followRedirect']}
- label="Follow redirect"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- </>
- )}
- {(protocol === Protocols.HTTP || protocol === Protocols.MIXED) && (
- <>
- <Form.List name={['settings', 'accounts']}>
- {(fields, { add, remove }) => (
- <>
- <Form.Item label="Accounts">
- <Button size="small" onClick={() => add({ user: '', pass: '' })}>
- <PlusOutlined /> Add
- </Button>
- </Form.Item>
- {fields.length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {fields.map((field, idx) => (
- <Space.Compact key={field.key} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Form.Item name={[field.name, 'user']} noStyle>
- <Input placeholder="Username" />
- </Form.Item>
- <Form.Item name={[field.name, 'pass']} noStyle>
- <Input placeholder="Password" />
- </Form.Item>
- <Button onClick={() => remove(field.name)}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </>
- )}
- </Form.List>
- {protocol === Protocols.HTTP && (
- <Form.Item
- name={['settings', 'allowTransparent']}
- label="Allow transparent"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- )}
- {protocol === Protocols.MIXED && (
- <>
- <Form.Item name={['settings', 'auth']} label="Auth">
- <Select>
- <Select.Option value="noauth">noauth</Select.Option>
- <Select.Option value="password">password</Select.Option>
- </Select>
- </Form.Item>
- <Form.Item
- name={['settings', 'udp']}
- label="UDP"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- {mixedUdpOn && (
- <Form.Item name={['settings', 'ip']} label="UDP IP">
- <Input />
- </Form.Item>
- )}
- </>
- )}
- </>
- )}
- {protocol === Protocols.SHADOWSOCKS && (
- <>
- <Form.Item name={['settings', 'method']} label="Encryption method">
- <Select
- onChange={(v) => {
- form.setFieldValue(
- ['settings', 'password'],
- RandomUtil.randomShadowsocksPassword(v as string),
- );
- }}
- >
- {SSMethodSchema.options.map((m) => (
- <Select.Option key={m} value={m}>{m}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- {isSSWith2022 && (
- <Form.Item
- name={['settings', 'password']}
- label={
- <>
- Password{' '}
- <SyncOutlined
- className="random-icon"
- onClick={() => {
- const method = form.getFieldValue(['settings', 'method']);
- form.setFieldValue(
- ['settings', 'password'],
- RandomUtil.randomShadowsocksPassword(method as string),
- );
- }}
- />
- </>
- }
- >
- <Input />
- </Form.Item>
- )}
- <Form.Item name={['settings', 'network']} label="Network">
- <Select style={{ width: 120 }}>
- <Select.Option value="tcp,udp">TCP, UDP</Select.Option>
- <Select.Option value="tcp">TCP</Select.Option>
- <Select.Option value="udp">UDP</Select.Option>
- </Select>
- </Form.Item>
- <Form.Item
- name={['settings', 'ivCheck']}
- label="ivCheck"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- </>
- )}
- {protocol === Protocols.VLESS && (
- <>
- <Form.Item name={['settings', 'decryption']} label={t('pages.inbounds.decryption')}>
- <Input />
- </Form.Item>
- <Form.Item name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
- <Input />
- </Form.Item>
- <Form.Item label=" ">
- <Space size={8} wrap>
- <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
- {t('pages.inbounds.vlessAuthX25519')}
- </Button>
- <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
- {t('pages.inbounds.vlessAuthMlkem768')}
- </Button>
- <Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
- </Space>
- <Text type="secondary" className="vless-auth-state">
- {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
- </Text>
- </Form.Item>
- </>
- )}
- {isFallbackHost && fallbacksCard}
- </>
- );
- // Switching `network` swaps which per-network key (tcpSettings, wsSettings,
- // grpcSettings, ...) appears on the wire. We clear the previously selected
- // network's settings blob and seed a default empty object for the new one
- // so AntD's Form.Items aren't pointed at undefined nested paths.
- const onNetworkChange = (next: string) => {
- const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings'];
- const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
- const cleaned: Record<string, unknown> = { ...current, network: next };
- for (const k of ALL) {
- if (k !== `${next}Settings`) delete cleaned[k];
- }
- cleaned[`${next}Settings`] = {};
- form.setFieldValue('streamSettings', cleaned);
- };
- const streamTab = (
- <>
- {protocol !== Protocols.HYSTERIA && (
- <Form.Item label="Transmission">
- <Select
- value={network}
- style={{ width: '75%' }}
- onChange={onNetworkChange}
- >
- <Select.Option value="tcp">TCP (RAW)</Select.Option>
- <Select.Option value="kcp">mKCP</Select.Option>
- <Select.Option value="ws">WebSocket</Select.Option>
- <Select.Option value="grpc">gRPC</Select.Option>
- <Select.Option value="httpupgrade">HTTPUpgrade</Select.Option>
- <Select.Option value="xhttp">XHTTP</Select.Option>
- </Select>
- </Form.Item>
- )}
- {network === 'tcp' && (
- <>
- <Form.Item
- name={['streamSettings', 'tcpSettings', 'acceptProxyProtocol']}
- label="Proxy Protocol"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item label={`HTTP ${t('camouflage')}`}>
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) =>
- prev.streamSettings?.tcpSettings?.header?.type
- !== curr.streamSettings?.tcpSettings?.header?.type
- }
- >
- {({ getFieldValue, setFieldValue }) => {
- const headerType = getFieldValue(
- ['streamSettings', 'tcpSettings', 'header', 'type'],
- ) as string | undefined;
- return (
- <Switch
- checked={headerType === 'http'}
- onChange={(v) => {
- setFieldValue(
- ['streamSettings', 'tcpSettings', 'header'],
- v
- ? {
- type: 'http',
- request: {
- version: '1.1',
- method: 'GET',
- path: ['/'],
- headers: {},
- },
- response: {
- version: '1.1',
- status: '200',
- reason: 'OK',
- headers: {},
- },
- }
- : { type: 'none' },
- );
- }}
- />
- );
- }}
- </Form.Item>
- </Form.Item>
- {/* Host + path camouflage inputs only render when the Switch
- above is on. Both are string[] on the wire; normalize +
- getValueProps translate to/from comma-joined input. Mirrors
- the symmetric outbound side. */}
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) =>
- prev.streamSettings?.tcpSettings?.header?.type
- !== curr.streamSettings?.tcpSettings?.header?.type
- }
- >
- {({ getFieldValue }) => {
- const headerType = getFieldValue(
- ['streamSettings', 'tcpSettings', 'header', 'type'],
- ) as string | undefined;
- if (headerType !== 'http') return null;
- return (
- <>
- <Form.Item
- label={t('host')}
- name={[
- 'streamSettings', 'tcpSettings', 'header',
- 'request', 'headers', 'Host',
- ]}
- normalize={(v: unknown) =>
- typeof v === 'string'
- ? v.split(',').map((s) => s.trim()).filter(Boolean)
- : Array.isArray(v) ? v : []
- }
- getValueProps={(v: unknown) => ({
- value: Array.isArray(v) ? v.join(',') : '',
- })}
- >
- <Input placeholder="example.com,cdn.example.com" />
- </Form.Item>
- <Form.Item
- label={t('path')}
- name={[
- 'streamSettings', 'tcpSettings', 'header',
- 'request', 'path',
- ]}
- normalize={(v: unknown) =>
- typeof v === 'string'
- ? v.split(',').map((s) => s.trim()).filter(Boolean)
- : Array.isArray(v) ? v : ['/']
- }
- getValueProps={(v: unknown) => ({
- value: Array.isArray(v) ? v.join(',') : '/',
- })}
- >
- <Input placeholder="/,/api,/static" />
- </Form.Item>
- <Form.Item
- label="Request headers"
- name={[
- 'streamSettings', 'tcpSettings', 'header',
- 'request', 'headers',
- ]}
- >
- <HeaderMapEditor mode="v2" />
- </Form.Item>
- {/* Response side: shaped as a separate sub-object on the
- wire ({version, status, reason, headers}). Inbound is
- the server, so the response side is the one the panel
- sends back to clients during HTTP camouflage. */}
- <Form.Item
- label="Response version"
- name={[
- 'streamSettings', 'tcpSettings', 'header',
- 'response', 'version',
- ]}
- >
- <Input placeholder="1.1" />
- </Form.Item>
- <Form.Item
- label="Response status"
- name={[
- 'streamSettings', 'tcpSettings', 'header',
- 'response', 'status',
- ]}
- >
- <Input placeholder="200" />
- </Form.Item>
- <Form.Item
- label="Response reason"
- name={[
- 'streamSettings', 'tcpSettings', 'header',
- 'response', 'reason',
- ]}
- >
- <Input placeholder="OK" />
- </Form.Item>
- <Form.Item
- label="Response headers"
- name={[
- 'streamSettings', 'tcpSettings', 'header',
- 'response', 'headers',
- ]}
- >
- <HeaderMapEditor mode="v2" />
- </Form.Item>
- </>
- );
- }}
- </Form.Item>
- </>
- )}
- {network === 'ws' && (
- <>
- <Form.Item
- name={['streamSettings', 'wsSettings', 'acceptProxyProtocol']}
- label="Proxy Protocol"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item name={['streamSettings', 'wsSettings', 'host']} label={t('host')}>
- <Input />
- </Form.Item>
- <Form.Item name={['streamSettings', 'wsSettings', 'path']} label={t('path')}>
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'wsSettings', 'heartbeatPeriod']}
- label="Heartbeat Period"
- >
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- label="Headers"
- name={['streamSettings', 'wsSettings', 'headers']}
- >
- <HeaderMapEditor mode="v1" />
- </Form.Item>
- </>
- )}
- {network === 'grpc' && (
- <>
- <Form.Item
- name={['streamSettings', 'grpcSettings', 'serviceName']}
- label="Service Name"
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'grpcSettings', 'authority']}
- label="Authority"
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'grpcSettings', 'multiMode']}
- label="Multi Mode"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- </>
- )}
- {network === 'xhttp' && (
- <>
- <Form.Item name={['streamSettings', 'xhttpSettings', 'host']} label={t('host')}>
- <Input />
- </Form.Item>
- <Form.Item name={['streamSettings', 'xhttpSettings', 'path']} label={t('path')}>
- <Input />
- </Form.Item>
- <Form.Item name={['streamSettings', 'xhttpSettings', 'mode']} label="Mode">
- <Select style={{ width: '50%' }}>
- {(['auto', 'packet-up', 'stream-up', 'stream-one'] as const).map((m) => (
- <Select.Option key={m} value={m}>{m}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- {xhttpMode === 'packet-up' && (
- <>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'scMaxBufferedPosts']}
- label="Max Buffered Upload"
- >
- <InputNumber />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
- label="Max Upload Size (Byte)"
- >
- <Input />
- </Form.Item>
- </>
- )}
- {xhttpMode === 'stream-up' && (
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'scStreamUpServerSecs']}
- label="Stream-Up Server"
- >
- <Input />
- </Form.Item>
- )}
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'serverMaxHeaderBytes']}
- label="Server Max Header Bytes"
- >
- <InputNumber min={0} placeholder="0 (default)" />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
- label="Padding Bytes"
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
- label="Uplink HTTP Method"
- >
- <Select>
- <Select.Option value="">Default (POST)</Select.Option>
- <Select.Option value="POST">POST</Select.Option>
- <Select.Option value="PUT">PUT</Select.Option>
- <Select.Option value="GET" disabled={xhttpMode !== 'packet-up'}>
- GET (packet-up only)
- </Select.Option>
- </Select>
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
- label="Padding Obfs Mode"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- {xhttpObfsMode && (
- <>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
- label="Padding Key"
- >
- <Input placeholder="x_padding" />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
- label="Padding Header"
- >
- <Input placeholder="X-Padding" />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
- label="Padding Placement"
- >
- <Select>
- <Select.Option value="">Default (queryInHeader)</Select.Option>
- <Select.Option value="queryInHeader">queryInHeader</Select.Option>
- <Select.Option value="header">header</Select.Option>
- <Select.Option value="cookie">cookie</Select.Option>
- <Select.Option value="query">query</Select.Option>
- </Select>
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
- label="Padding Method"
- >
- <Select>
- <Select.Option value="">Default (repeat-x)</Select.Option>
- <Select.Option value="repeat-x">repeat-x</Select.Option>
- <Select.Option value="tokenish">tokenish</Select.Option>
- </Select>
- </Form.Item>
- </>
- )}
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
- label="Session Placement"
- >
- <Select>
- <Select.Option value="">Default (path)</Select.Option>
- <Select.Option value="path">path</Select.Option>
- <Select.Option value="header">header</Select.Option>
- <Select.Option value="cookie">cookie</Select.Option>
- <Select.Option value="query">query</Select.Option>
- </Select>
- </Form.Item>
- {xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && (
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'sessionKey']}
- label="Session Key"
- >
- <Input placeholder="x_session" />
- </Form.Item>
- )}
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
- label="Sequence Placement"
- >
- <Select>
- <Select.Option value="">Default (path)</Select.Option>
- <Select.Option value="path">path</Select.Option>
- <Select.Option value="header">header</Select.Option>
- <Select.Option value="cookie">cookie</Select.Option>
- <Select.Option value="query">query</Select.Option>
- </Select>
- </Form.Item>
- {xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && (
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'seqKey']}
- label="Sequence Key"
- >
- <Input placeholder="x_seq" />
- </Form.Item>
- )}
- {xhttpMode === 'packet-up' && (
- <>
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
- label="Uplink Data Placement"
- >
- <Select>
- <Select.Option value="">Default (body)</Select.Option>
- <Select.Option value="body">body</Select.Option>
- <Select.Option value="header">header</Select.Option>
- <Select.Option value="cookie">cookie</Select.Option>
- <Select.Option value="query">query</Select.Option>
- </Select>
- </Form.Item>
- {xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && (
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
- label="Uplink Data Key"
- >
- <Input placeholder="x_data" />
- </Form.Item>
- )}
- </>
- )}
- <Form.Item
- name={['streamSettings', 'xhttpSettings', 'noSSEHeader']}
- label="No SSE Header"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- </>
- )}
- {network === 'httpupgrade' && (
- <>
- <Form.Item
- name={['streamSettings', 'httpupgradeSettings', 'acceptProxyProtocol']}
- label="Proxy Protocol"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'httpupgradeSettings', 'host']}
- label={t('host')}
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'httpupgradeSettings', 'path']}
- label={t('path')}
- >
- <Input />
- </Form.Item>
- <Form.Item
- label="Headers"
- name={['streamSettings', 'httpupgradeSettings', 'headers']}
- >
- <HeaderMapEditor mode="v1" />
- </Form.Item>
- </>
- )}
- {network === 'kcp' && (
- <>
- <Form.Item name={['streamSettings', 'kcpSettings', 'mtu']} label="MTU">
- <InputNumber min={576} max={1460} />
- </Form.Item>
- <Form.Item name={['streamSettings', 'kcpSettings', 'tti']} label="TTI (ms)">
- <InputNumber min={10} max={100} />
- </Form.Item>
- <Form.Item name={['streamSettings', 'kcpSettings', 'upCap']} label="Uplink (MB/s)">
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item name={['streamSettings', 'kcpSettings', 'downCap']} label="Downlink (MB/s)">
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
- label="CWND Multiplier"
- >
- <InputNumber min={1} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
- label="Max Sending Window"
- >
- <InputNumber min={0} />
- </Form.Item>
- </>
- )}
- <Form.Item label="External Proxy">
- <Switch checked={externalProxyOn} onChange={toggleExternalProxy} />
- </Form.Item>
- {externalProxyOn && (
- <Form.List name={['streamSettings', 'externalProxy']}>
- {(fields, { add, remove }) => (
- <>
- <Form.Item label=" " colon={false}>
- <Button
- size="small"
- type="primary"
- onClick={() => add({
- forceTls: 'same',
- dest: '',
- port: 443,
- remark: '',
- sni: '',
- fingerprint: '',
- alpn: [],
- })}
- >
- <PlusOutlined />
- </Button>
- </Form.Item>
- <Form.Item wrapperCol={{ span: 24 }}>
- {fields.map((field) => (
- <div key={field.key} style={{ margin: '8px 0' }}>
- <Space.Compact block>
- <Form.Item name={[field.name, 'forceTls']} noStyle>
- <Select style={{ width: '20%' }}>
- <Select.Option value="same">{t('pages.inbounds.same')}</Select.Option>
- <Select.Option value="none">{t('none')}</Select.Option>
- <Select.Option value="tls">TLS</Select.Option>
- </Select>
- </Form.Item>
- <Form.Item name={[field.name, 'dest']} noStyle>
- <Input style={{ width: '30%' }} placeholder={t('host')} />
- </Form.Item>
- <Form.Item name={[field.name, 'port']} noStyle>
- <InputNumber style={{ width: '15%' }} min={1} max={65535} />
- </Form.Item>
- <Form.Item name={[field.name, 'remark']} noStyle>
- <Input style={{ width: '25%' }} placeholder={t('pages.inbounds.remark')} />
- </Form.Item>
- <InputAddon onClick={() => remove(field.name)}>
- <MinusOutlined />
- </InputAddon>
- </Space.Compact>
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) =>
- prev.streamSettings?.externalProxy?.[field.name]?.forceTls
- !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
- }
- >
- {({ getFieldValue }) => {
- const ft = getFieldValue([
- 'streamSettings', 'externalProxy', field.name, 'forceTls',
- ]);
- if (ft !== 'tls') return null;
- return (
- <Space.Compact style={{ marginTop: 6 }} block>
- <Form.Item name={[field.name, 'sni']} noStyle>
- <Input style={{ width: '30%' }} placeholder="SNI (defaults to host)" />
- </Form.Item>
- <Form.Item name={[field.name, 'fingerprint']} noStyle>
- <Select style={{ width: '30%' }} placeholder="Fingerprint">
- <Select.Option value="">Default</Select.Option>
- {Object.values(UTLS_FINGERPRINT).map((fp) => (
- <Select.Option key={fp} value={fp}>{fp}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item name={[field.name, 'alpn']} noStyle>
- <Select mode="multiple" style={{ width: '40%' }} placeholder="ALPN">
- {Object.values(ALPN_OPTION).map((a) => (
- <Select.Option key={a} value={a}>{a}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- </Space.Compact>
- );
- }}
- </Form.Item>
- </div>
- ))}
- </Form.Item>
- </>
- )}
- </Form.List>
- )}
- <Form.Item label="Sockopt">
- <Switch checked={sockoptOn} onChange={toggleSockopt} />
- </Form.Item>
- {sockoptOn && (
- <>
- <Form.Item name={['streamSettings', 'sockopt', 'mark']} label="Route Mark">
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
- label="TCP Keep Alive Interval"
- >
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
- label="TCP Keep Alive Idle"
- >
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item name={['streamSettings', 'sockopt', 'tcpMaxSeg']} label="TCP Max Seg">
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
- label="TCP User Timeout"
- >
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
- label="TCP Window Clamp"
- >
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
- label="Proxy Protocol"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'tcpFastOpen']}
- label="TCP Fast Open"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'tcpMptcp']}
- label="Multipath TCP"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'penetrate']}
- label="Penetrate"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'V6Only']}
- label="V6 Only"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'domainStrategy']}
- label="Domain Strategy"
- >
- <Select style={{ width: '50%' }}>
- {Object.values(DOMAIN_STRATEGY_OPTION).map((d) => (
- <Select.Option key={d} value={d}>{d}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'tcpcongestion']}
- label="TCP Congestion"
- >
- <Select style={{ width: '50%' }}>
- {Object.values(TCP_CONGESTION_OPTION).map((c) => (
- <Select.Option key={c} value={c}>{c}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
- <Select style={{ width: '50%' }}>
- <Select.Option value="off">Off</Select.Option>
- <Select.Option value="redirect">Redirect</Select.Option>
- <Select.Option value="tproxy">TProxy</Select.Option>
- </Select>
- </Form.Item>
- <Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label="Dialer Proxy">
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'interfaceName']}
- label="Interface Name"
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
- label="Trusted X-Forwarded-For"
- >
- <Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']}>
- <Select.Option value="CF-Connecting-IP">CF-Connecting-IP</Select.Option>
- <Select.Option value="X-Real-IP">X-Real-IP</Select.Option>
- <Select.Option value="True-Client-IP">True-Client-IP</Select.Option>
- <Select.Option value="X-Client-IP">X-Client-IP</Select.Option>
- </Select>
- </Form.Item>
- </>
- )}
- </>
- );
- const securityTab = (
- <>
- <Form.Item label={t('pages.inbounds.securityTab')}>
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) =>
- prev.streamSettings?.security !== curr.streamSettings?.security
- }
- >
- {({ getFieldValue }) => {
- const sec = getFieldValue(['streamSettings', 'security']) ?? 'none';
- return (
- <Select
- value={sec}
- disabled={!tlsAllowed}
- onChange={onSecurityChange}
- style={{ width: 180 }}
- >
- <Select.Option value="none">none</Select.Option>
- <Select.Option value="tls">tls</Select.Option>
- {realityAllowed && <Select.Option value="reality">reality</Select.Option>}
- </Select>
- );
- }}
- </Form.Item>
- </Form.Item>
- {security === 'tls' && (
- <>
- <Form.Item name={['streamSettings', 'tlsSettings', 'serverName']} label="SNI">
- <Input placeholder="Server Name Indication" />
- </Form.Item>
- <Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label="Cipher Suites">
- <Select>
- <Select.Option value="">Auto</Select.Option>
- {Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => (
- <Select.Option key={v} value={v}>{k}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item label="Min/Max Version">
- <Space.Compact block>
- <Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
- <Select style={{ width: '50%' }}>
- {Object.values(TLS_VERSION_OPTION).map((v) => (
- <Select.Option key={v} value={v}>{v}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
- <Select style={{ width: '50%' }}>
- {Object.values(TLS_VERSION_OPTION).map((v) => (
- <Select.Option key={v} value={v}>{v}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- </Space.Compact>
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']}
- label="uTLS"
- >
- <Select>
- <Select.Option value="">None</Select.Option>
- {Object.values(UTLS_FINGERPRINT).map((fp) => (
- <Select.Option key={fp} value={fp}>{fp}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN">
- <Select mode="multiple" tokenSeparators={[',']} style={{ width: '100%' }}>
- {Object.values(ALPN_OPTION).map((a) => (
- <Select.Option key={a} value={a}>{a}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
- label="Reject Unknown SNI"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'tlsSettings', 'disableSystemRoot']}
- label="Disable System Root"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'tlsSettings', 'enableSessionResumption']}
- label="Session Resumption"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.List name={['streamSettings', 'tlsSettings', 'certificates']}>
- {(certFields, { add, remove }) => (
- <>
- <Form.Item label={t('certificate')}>
- <Button
- type="primary"
- size="small"
- onClick={() => add({
- useFile: true,
- certificateFile: '',
- keyFile: '',
- certificate: [],
- key: [],
- oneTimeLoading: false,
- usage: 'encipherment',
- buildChain: false,
- })}
- >
- <PlusOutlined />
- </Button>
- </Form.Item>
- {certFields.map((certField, idx) => (
- <div key={certField.key}>
- <Form.Item
- name={[certField.name, 'useFile']}
- label={`${t('certificate')} ${idx + 1}`}
- >
- <Radio.Group buttonStyle="solid">
- <Radio.Button value={true}>
- {t('pages.inbounds.certificatePath')}
- </Radio.Button>
- <Radio.Button value={false}>
- {t('pages.inbounds.certificateContent')}
- </Radio.Button>
- </Radio.Group>
- </Form.Item>
- {certFields.length > 1 && (
- <Form.Item label=" ">
- <Button
- size="small"
- danger
- onClick={() => remove(certField.name)}
- >
- <MinusOutlined /> Remove
- </Button>
- </Form.Item>
- )}
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) =>
- prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
- !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
- }
- >
- {({ getFieldValue }) => {
- const useFile = getFieldValue([
- 'streamSettings', 'tlsSettings', 'certificates',
- certField.name, 'useFile',
- ]);
- return useFile ? (
- <>
- <Form.Item
- name={[certField.name, 'certificateFile']}
- label={t('pages.inbounds.publicKey')}
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={[certField.name, 'keyFile']}
- label={t('pages.inbounds.privatekey')}
- >
- <Input />
- </Form.Item>
- </>
- ) : (
- <>
- <Form.Item
- name={[certField.name, 'certificate']}
- label={t('pages.inbounds.publicKey')}
- normalize={(v) => typeof v === 'string'
- ? v.split('\n')
- : v}
- getValueProps={(v) => ({
- value: Array.isArray(v) ? v.join('\n') : v,
- })}
- >
- <TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
- </Form.Item>
- <Form.Item
- name={[certField.name, 'key']}
- label={t('pages.inbounds.privatekey')}
- normalize={(v) => typeof v === 'string'
- ? v.split('\n')
- : v}
- getValueProps={(v) => ({
- value: Array.isArray(v) ? v.join('\n') : v,
- })}
- >
- <TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
- </Form.Item>
- </>
- );
- }}
- </Form.Item>
- <Form.Item
- name={[certField.name, 'oneTimeLoading']}
- label="One Time Loading"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={[certField.name, 'usage']}
- label="Usage Option"
- >
- <Select style={{ width: '50%' }}>
- {Object.values(USAGE_OPTION).map((u) => (
- <Select.Option key={u} value={u}>{u}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- noStyle
- shouldUpdate={(prev, curr) =>
- prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
- !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
- }
- >
- {({ getFieldValue }) => {
- const usage = getFieldValue([
- 'streamSettings', 'tlsSettings', 'certificates',
- certField.name, 'usage',
- ]);
- if (usage !== 'issue') return null;
- return (
- <Form.Item
- name={[certField.name, 'buildChain']}
- label="Build Chain"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- );
- }}
- </Form.Item>
- </div>
- ))}
- </>
- )}
- </Form.List>
- <Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label="ECH key">
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
- label="ECH config"
- >
- <Input />
- </Form.Item>
- <Form.Item label=" ">
- <Space>
- <Button type="primary" loading={saving} onClick={getNewEchCert}>
- Get New ECH Cert
- </Button>
- <Button danger onClick={clearEchCert}>Clear</Button>
- </Space>
- </Form.Item>
- </>
- )}
- {security === 'reality' && (
- <>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'show']}
- label="Show"
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item name={['streamSettings', 'realitySettings', 'xver']} label="Xver">
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
- label="uTLS"
- >
- <Select>
- {Object.values(UTLS_FINGERPRINT).map((fp) => (
- <Select.Option key={fp} value={fp}>{fp}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'target']}
- label={
- <>
- Target{' '}
- <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} />
- </>
- }
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'serverNames']}
- label={
- <>
- SNI{' '}
- <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} />
- </>
- }
- >
- <Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'maxTimediff']}
- label="Max Time Diff (ms)"
- >
- <InputNumber min={0} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'minClientVer']}
- label="Min Client Ver"
- >
- <Input placeholder="25.9.11" />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'maxClientVer']}
- label="Max Client Ver"
- >
- <Input placeholder="25.9.11" />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'shortIds']}
- label={
- <>
- Short IDs{' '}
- <SyncOutlined className="random-icon" onClick={randomizeShortIds} />
- </>
- }
- >
- <Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
- label="SpiderX"
- >
- <Input />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
- label={t('pages.inbounds.publicKey')}
- >
- <Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'privateKey']}
- label={t('pages.inbounds.privatekey')}
- >
- <Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
- </Form.Item>
- <Form.Item label=" ">
- <Space>
- <Button type="primary" loading={saving} onClick={genRealityKeypair}>
- Get New Cert
- </Button>
- <Button danger onClick={clearRealityKeypair}>Clear</Button>
- </Space>
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'mldsa65Seed']}
- label="mldsa65 Seed"
- >
- <Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
- </Form.Item>
- <Form.Item
- name={['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify']}
- label="mldsa65 Verify"
- >
- <Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
- </Form.Item>
- <Form.Item label=" ">
- <Space>
- <Button type="primary" loading={saving} onClick={genMldsa65}>
- Get New Seed
- </Button>
- <Button danger onClick={clearMldsa65}>Clear</Button>
- </Space>
- </Form.Item>
- </>
- )}
- </>
- );
- const advancedTab = (
- <Tabs
- items={[
- {
- key: 'settings',
- label: t('pages.inbounds.advanced.settings'),
- children: (
- <AdvancedSliceEditor
- form={form}
- path="settings"
- minHeight="320px"
- maxHeight="540px"
- />
- ),
- },
- ...(streamEnabled
- ? [{
- key: 'stream',
- label: t('pages.inbounds.advanced.stream'),
- children: (
- <AdvancedSliceEditor
- form={form}
- path="streamSettings"
- minHeight="320px"
- maxHeight="540px"
- />
- ),
- }]
- : []),
- {
- key: 'sniffing',
- label: t('pages.inbounds.advanced.sniffing'),
- children: (
- <AdvancedSliceEditor
- form={form}
- path="sniffing"
- minHeight="240px"
- maxHeight="420px"
- />
- ),
- },
- ]}
- />
- );
- const sniffingTab = (
- <>
- <Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
- <Switch />
- </Form.Item>
- {sniffingEnabled && (
- <>
- <Form.Item name={['sniffing', 'destOverride']} wrapperCol={{ span: 24 }}>
- <Checkbox.Group>
- {Object.entries(SNIFFING_OPTION).map(([key, value]) => (
- <Checkbox key={key} value={value}>{key}</Checkbox>
- ))}
- </Checkbox.Group>
- </Form.Item>
- <Form.Item
- name={['sniffing', 'metadataOnly']}
- label={t('pages.inbounds.sniffingMetadataOnly')}
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['sniffing', 'routeOnly']}
- label={t('pages.inbounds.sniffingRouteOnly')}
- valuePropName="checked"
- >
- <Switch />
- </Form.Item>
- <Form.Item
- name={['sniffing', 'ipsExcluded']}
- label={t('pages.inbounds.sniffingIpsExcluded')}
- >
- <Select
- mode="tags"
- tokenSeparators={[',']}
- placeholder="IP/CIDR/geoip:*/ext:*"
- style={{ width: '100%' }}
- />
- </Form.Item>
- <Form.Item
- name={['sniffing', 'domainsExcluded']}
- label={t('pages.inbounds.sniffingDomainsExcluded')}
- >
- <Select
- mode="tags"
- tokenSeparators={[',']}
- placeholder="domain:*/ext:*"
- style={{ width: '100%' }}
- />
- </Form.Item>
- </>
- )}
- </>
- );
- return (
- <>
- {messageContextHolder}
- <Modal
- open={open}
- title={title}
- okText={okText}
- cancelText={t('close')}
- confirmLoading={saving}
- mask={{ closable: false }}
- width={780}
- onOk={submit}
- onCancel={onClose}
- destroyOnHidden
- >
- <Form
- form={form}
- colon={false}
- labelCol={{ sm: { span: 8 } }}
- wrapperCol={{ sm: { span: 14 } }}
- onValuesChange={onValuesChange}
- >
- <Tabs items={[
- { key: 'basic', label: t('pages.xray.basicTemplate'), children: basicTab },
- ...(([
- Protocols.VLESS,
- Protocols.SHADOWSOCKS,
- Protocols.HTTP,
- Protocols.MIXED,
- Protocols.TUNNEL,
- Protocols.TUN,
- Protocols.WIREGUARD,
- ] as string[]).includes(protocol) || isFallbackHost
- ? [{ key: 'protocol', label: t('pages.inbounds.protocol'), children: protocolTab }]
- : []),
- ...(streamEnabled
- ? [
- { key: 'stream', label: t('pages.inbounds.streamTab'), children: streamTab },
- { key: 'security', label: t('pages.inbounds.securityTab'), children: securityTab },
- ]
- : []),
- { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab },
- { key: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab },
- ]} />
- </Form>
- </Modal>
- </>
- );
- }
|