| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262 |
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
- import { useTranslation } from 'react-i18next';
- import dayjs, { type Dayjs } from 'dayjs';
- import {
- Button,
- Card,
- Checkbox,
- Col,
- Divider,
- Empty,
- Form,
- Input,
- InputNumber,
- Modal,
- Radio,
- Row,
- Select,
- Space,
- Switch,
- Tabs,
- Tooltip,
- Typography,
- message,
- } from 'antd';
- import {
- SyncOutlined,
- PlusOutlined,
- MinusOutlined,
- DeleteOutlined,
- CaretUpOutlined,
- CaretDownOutlined,
- SettingOutlined,
- } from '@ant-design/icons';
- import {
- HttpUtil,
- RandomUtil,
- NumberFormatter,
- SizeFormatter,
- Wireguard,
- } from '@/utils';
- import InputAddon from '@/components/InputAddon';
- import { getRandomRealityTarget } from '@/models/reality-targets';
- import {
- Inbound,
- Protocols,
- SSMethods,
- SNIFFING_OPTION,
- TLS_VERSION_OPTION,
- TLS_CIPHER_OPTION,
- UTLS_FINGERPRINT,
- ALPN_OPTION,
- USAGE_OPTION,
- DOMAIN_STRATEGY_OPTION,
- TCP_CONGESTION_OPTION,
- MODE_OPTION,
- } from '@/models/inbound';
- import { DBInbound } from '@/models/dbinbound';
- import FinalMaskForm from '@/components/FinalMaskForm';
- import DateTimePicker from '@/components/DateTimePicker';
- import JsonEditor from '@/components/JsonEditor';
- import type { NodeRecord } from '@/api/queries/useNodesQuery';
- import { InboundFormSchema } from '@/schemas/inbound';
- import './InboundFormModal.css';
- const { TextArea } = Input;
- const { Text, Paragraph } = Typography;
- interface InboundFormModalProps {
- open: boolean;
- onClose: () => void;
- onSaved: () => void;
- mode: 'add' | 'edit';
- dbInbound: DBInbound | null;
- dbInbounds: DBInbound[];
- availableNodes?: NodeRecord[];
- }
- interface StreamLike {
- network?: string;
- tcp?: { type?: string; request?: { path?: string[] }; acceptProxyProtocol?: boolean };
- ws?: { path?: string; acceptProxyProtocol?: boolean };
- grpc?: { serviceName?: string; multiMode?: boolean };
- httpupgrade?: { path?: string; acceptProxyProtocol?: boolean };
- xhttp?: { path?: string };
- security?: string;
- tls?: { certs?: TlsCert[] };
- reality?: unknown;
- externalProxy?: unknown;
- }
- interface TlsCert {
- useFile?: boolean;
- certFile?: string;
- keyFile?: string;
- cert?: string;
- key?: string;
- ocspStapling?: number;
- oneTimeLoading?: boolean;
- usage?: string;
- buildChain?: boolean;
- }
- interface VlessClient {
- id?: string;
- email?: string;
- flow?: string;
- enable?: boolean;
- subId?: string;
- totalGB?: number;
- expiryTime?: number;
- limitIp?: number;
- comment?: string;
- tgId?: string;
- }
- interface ShadowsocksClient {
- email?: string;
- password?: string;
- method?: string;
- enable?: boolean;
- subId?: string;
- totalGB?: number;
- expiryTime?: number;
- limitIp?: number;
- comment?: string;
- tgId?: string;
- }
- interface HttpAccount {
- user?: string;
- pass?: string;
- }
- interface WireguardPeer {
- privateKey?: string;
- publicKey?: string;
- psk?: string;
- allowedIPs: string[];
- keepAlive?: number;
- }
- const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'];
- const PROTOCOLS = Object.values(Protocols) as string[];
- const TLS_VERSIONS = Object.values(TLS_VERSION_OPTION) as string[];
- const CIPHER_SUITES = Object.entries(TLS_CIPHER_OPTION) as [string, string][];
- const FINGERPRINTS = Object.values(UTLS_FINGERPRINT) as string[];
- const ALPNS = Object.values(ALPN_OPTION) as string[];
- const USAGES = Object.values(USAGE_OPTION) as string[];
- const DOMAIN_STRATEGIES = Object.values(DOMAIN_STRATEGY_OPTION) as string[];
- const TCP_CONGESTIONS = Object.values(TCP_CONGESTION_OPTION) as string[];
- const MODE_OPTIONS = Object.values(MODE_OPTION) as string[];
- const NODE_ELIGIBLE_PROTOCOLS = new Set([
- Protocols.VLESS,
- Protocols.VMESS,
- Protocols.TROJAN,
- Protocols.SHADOWSOCKS,
- Protocols.HYSTERIA,
- Protocols.WIREGUARD,
- ]);
- const FALLBACK_ELIGIBLE_TRANSPORTS = new Set(['tcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']);
- interface FallbackRow {
- rowKey: string;
- childId: number | null;
- name: string;
- alpn: string;
- path: string;
- xver: number;
- }
- function deriveFallbackDefaults(childDb: DBInbound | null | undefined): Omit<FallbackRow, 'rowKey' | 'childId'> {
- const out = { name: '', alpn: '', path: '', xver: 0 };
- if (!childDb) return out;
- let stream: StreamLike | undefined;
- try {
- stream = childDb.toInbound()?.stream as StreamLike | undefined;
- } catch {
- return out;
- }
- if (!stream) return out;
- switch (stream.network) {
- case 'tcp': {
- const tcp = stream.tcp;
- if (tcp?.type === 'http') {
- const p = tcp?.request?.path;
- if (Array.isArray(p) && p.length) out.path = p[0];
- }
- if (tcp?.acceptProxyProtocol) out.xver = 2;
- break;
- }
- case 'ws': {
- out.path = stream.ws?.path || '';
- if (stream.ws?.acceptProxyProtocol) out.xver = 2;
- break;
- }
- case 'grpc': {
- out.path = stream.grpc?.serviceName || '';
- out.alpn = 'h2';
- break;
- }
- case 'httpupgrade': {
- out.path = stream.httpupgrade?.path || '';
- if (stream.httpupgrade?.acceptProxyProtocol) out.xver = 2;
- break;
- }
- case 'xhttp': {
- out.path = stream.xhttp?.path || '';
- break;
- }
- }
- return out;
- }
- export default function InboundFormModal({
- open,
- onClose,
- onSaved,
- mode,
- dbInbound,
- dbInbounds,
- availableNodes,
- }: InboundFormModalProps) {
- const { t } = useTranslation();
- const [messageApi, messageContextHolder] = message.useMessage();
- const selectableNodes = useMemo(
- () => (availableNodes || []).filter((n: NodeRecord) => n.enable),
- [availableNodes],
- );
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const inboundRef = useRef<any>(null);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const dbFormRef = useRef<any>(null);
- const fallbackKeyRef = useRef(0);
- const advancedTextRef = useRef({ stream: '', sniffing: '', settings: '' });
- const [, setTick] = useState(0);
- const refresh = useCallback(() => setTick((n) => n + 1), []);
- const [saving, setSaving] = useState(false);
- const [activeTabKey, setActiveTabKey] = useState('basic');
- const [advancedSectionKey, setAdvancedSectionKey] = useState('all');
- const [defaultCert, setDefaultCert] = useState('');
- const [defaultKey, setDefaultKey] = useState('');
- const [fallbacks, setFallbacks] = useState<FallbackRow[]>([]);
- const [fallbackEditing, setFallbackEditing] = useState<Set<string>>(new Set());
- const isVlessLike = inboundRef.current?.protocol === Protocols.VLESS;
- const isFallbackHost = useMemo(() => {
- const ib = inboundRef.current;
- if (!ib) return false;
- if (ib.protocol !== Protocols.VLESS && ib.protocol !== Protocols.TROJAN) return false;
- if (ib.stream?.network !== 'tcp') return false;
- const sec = ib.stream?.security;
- return sec === 'tls' || sec === 'reality';
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [inboundRef.current?.protocol, inboundRef.current?.stream?.network, inboundRef.current?.stream?.security]);
- const canEnableStream = inboundRef.current?.canEnableStream?.() === true;
- const canEnableTls = inboundRef.current?.canEnableTls?.() === true;
- const canEnableReality = inboundRef.current?.canEnableReality?.() === true;
- const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(inboundRef.current?.protocol);
- const hasProtocolTabContent = useMemo(() => {
- const ib = inboundRef.current;
- if (!ib) return false;
- if (ib.protocol === Protocols.VLESS) return true;
- if (isFallbackHost) return true;
- switch (ib.protocol) {
- case Protocols.SHADOWSOCKS:
- case Protocols.HTTP:
- case Protocols.MIXED:
- case Protocols.TUNNEL:
- case Protocols.TUN:
- case Protocols.WIREGUARD:
- return true;
- default:
- return false;
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [inboundRef.current?.protocol, isFallbackHost]);
- const externalProxyOn = Array.isArray(inboundRef.current?.stream?.externalProxy)
- && inboundRef.current.stream.externalProxy.length > 0;
- const stampAdvancedTextFor = useCallback((slice: 'stream' | 'sniffing' | 'settings') => {
- const ib = inboundRef.current;
- if (!ib) return;
- if (slice === 'stream' && !ib.canEnableStream?.()) {
- advancedTextRef.current.stream = '{}';
- return;
- }
- const obj = ib[slice];
- if (!obj) return;
- try {
- advancedTextRef.current[slice] = JSON.stringify(JSON.parse(obj.toString()), null, 2);
- } catch {
- /* keep prior */
- }
- }, []);
- const primeAdvancedJson = useCallback(() => {
- (['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor);
- }, [stampAdvancedTextFor]);
- const loadFallbacks = useCallback(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 fetchDefaultCertSettings = useCallback(async () => {
- try {
- const msg = await HttpUtil.post('/panel/setting/defaultSettings');
- if (msg?.success && msg.obj) {
- const obj = msg.obj as { defaultCert?: string; defaultKey?: string };
- setDefaultCert(obj.defaultCert || '');
- setDefaultKey(obj.defaultKey || '');
- }
- } catch {
- /* non-fatal */
- }
- }, []);
- useEffect(() => {
- if (!open) return;
- setFallbackEditing(new Set());
- if (mode === 'edit' && dbInbound) {
- const parsed = Inbound.fromJson(dbInbound.toInbound().toJson());
- inboundRef.current = parsed;
- dbFormRef.current = new DBInbound(dbInbound);
- primeAdvancedJson();
- if (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN) {
- loadFallbacks(dbInbound.id);
- } else {
- setFallbacks([]);
- }
- } else {
- const ib = new Inbound();
- ib.protocol = Protocols.VLESS;
- ib.settings = Inbound.Settings.getSettings(Protocols.VLESS);
- ib.port = RandomUtil.randomInteger(10000, 60000);
- inboundRef.current = ib;
- const form = new DBInbound();
- form.enable = true;
- form.remark = '';
- form.total = 0;
- form.expiryTime = 0;
- form.trafficReset = 'never';
- dbFormRef.current = form;
- primeAdvancedJson();
- setFallbacks([]);
- }
- setActiveTabKey('basic');
- setAdvancedSectionKey('all');
- fetchDefaultCertSettings();
- refresh();
- }, [open, mode, dbInbound, primeAdvancedJson, loadFallbacks, fetchDefaultCertSettings, refresh]);
- const setExternalProxy = useCallback((on: boolean) => {
- const ib = inboundRef.current;
- if (!ib?.stream) return;
- if (on) {
- ib.stream.externalProxy = [{
- forceTls: 'same',
- dest: window.location.hostname,
- port: ib.port,
- remark: '',
- sni: '',
- fingerprint: '',
- alpn: [],
- }];
- } else {
- ib.stream.externalProxy = [];
- }
- refresh();
- }, [refresh]);
- const onProtocolChange = useCallback((next: string) => {
- const ib = inboundRef.current;
- if (mode === 'edit' || !ib) return;
- ib.protocol = next;
- ib.settings = Inbound.Settings.getSettings(next);
- if (!NODE_ELIGIBLE_PROTOCOLS.has(next) && dbFormRef.current) {
- dbFormRef.current.nodeId = null;
- }
- primeAdvancedJson();
- refresh();
- }, [mode, primeAdvancedJson, refresh]);
- const onNetworkChange = useCallback((next: string) => {
- const ib = inboundRef.current;
- if (!ib?.stream) return;
- ib.stream.network = next;
- if (!ib.canEnableTls()) ib.stream.security = 'none';
- if (!ib.canEnableReality()) ib.reality = false;
- if (
- ib.protocol === Protocols.VLESS
- && !ib.canEnableTlsFlow()
- && Array.isArray(ib.settings.vlesses)
- ) {
- ib.settings.vlesses.forEach((c: VlessClient) => { c.flow = ''; });
- }
- if (next !== 'kcp' && ib.stream.finalmask) {
- ib.stream.finalmask.udp = [];
- }
- stampAdvancedTextFor('stream');
- refresh();
- }, [stampAdvancedTextFor, refresh]);
- const setSecurity = useCallback((v: string) => {
- const ib = inboundRef.current;
- if (ib?.stream) {
- ib.stream.security = v;
- refresh();
- }
- }, [refresh]);
- const addFallback = useCallback((childId: number | null = null) => {
- const row: FallbackRow = {
- rowKey: `fb-${++fallbackKeyRef.current}`,
- childId: childId || null,
- name: '',
- alpn: '',
- path: '',
- xver: 0,
- };
- if (childId) {
- const child = (dbInbounds || []).find((ib) => ib.id === childId);
- Object.assign(row, deriveFallbackDefaults(child));
- }
- setFallbacks((prev) => [...prev, row]);
- }, [dbInbounds]);
- const removeFallback = useCallback((idx: number) => {
- setFallbacks((prev) => prev.filter((_, i) => i !== idx));
- }, []);
- const moveFallback = useCallback((idx: number, dir: number) => {
- setFallbacks((prev) => {
- const arr = [...prev];
- const j = idx + dir;
- if (j < 0 || j >= arr.length) return prev;
- [arr[idx], arr[j]] = [arr[j], arr[idx]];
- return arr;
- });
- }, []);
- const onFallbackChildPicked = useCallback((rowKey: string, childId: number) => {
- setFallbacks((prev) => prev.map((row) => {
- if (row.rowKey !== rowKey) return row;
- const child = (dbInbounds || []).find((ib) => ib.id === childId);
- const defaults = deriveFallbackDefaults(child);
- return { ...row, childId, ...defaults };
- }));
- }, [dbInbounds]);
- const updateFallback = useCallback((rowKey: string, patch: Partial<FallbackRow>) => {
- setFallbacks((prev) => prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch } : row)));
- }, []);
- const rederiveFallback = useCallback((rowKey: string) => {
- setFallbacks((prev) => prev.map((row) => {
- if (row.rowKey !== rowKey || !row.childId) return row;
- const child = (dbInbounds || []).find((ib) => ib.id === row.childId);
- const defaults = deriveFallbackDefaults(child);
- return { ...row, ...defaults };
- }));
- messageApi.success(t('pages.inbounds.fallbacks.rederived') || 'Re-filled from child');
- }, [dbInbounds, t, messageApi]);
- const quickAddAllFallbacks = useCallback(() => {
- const masterId = dbInbound?.id;
- const list = dbInbounds || [];
- setFallbacks((prev) => {
- const existing = new Set(prev.map((r) => r.childId).filter(Boolean));
- const next = [...prev];
- let added = 0;
- for (const ib of list) {
- if (ib.id === masterId) continue;
- if (existing.has(ib.id)) continue;
- let stream: StreamLike | undefined;
- try { stream = ib.toInbound()?.stream as StreamLike | undefined; } catch { continue; }
- if (!stream || !FALLBACK_ELIGIBLE_TRANSPORTS.has(stream.network ?? '')) continue;
- const row: FallbackRow = {
- rowKey: `fb-${++fallbackKeyRef.current}`,
- childId: ib.id,
- ...deriveFallbackDefaults(ib),
- };
- next.push(row);
- added += 1;
- }
- if (added > 0) {
- messageApi.success(t('pages.inbounds.fallbacks.quickAdded', { n: added }) || `Added ${added} fallback(s)`);
- } else {
- messageApi.info(t('pages.inbounds.fallbacks.quickAddedNone') || 'No new eligible inbounds to add');
- }
- return next;
- });
- }, [dbInbound, dbInbounds, t, messageApi]);
- const fallbackChildOptions = useMemo(() => {
- const list = dbInbounds || [];
- const masterId = dbInbound?.id;
- return list
- .filter((ib) => ib.id !== masterId)
- .map((ib) => ({
- label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
- value: ib.id,
- }));
- }, [dbInbounds, dbInbound]);
- const toggleFallbackEdit = useCallback((rowKey: string) => {
- setFallbackEditing((prev) => {
- const next = new Set(prev);
- if (next.has(rowKey)) next.delete(rowKey); else next.add(rowKey);
- return next;
- });
- }, []);
- const describeFallback = useCallback((record: FallbackRow) => {
- const parts: string[] = [];
- if (record.name) parts.push(`SNI=${record.name}`);
- if (record.alpn) parts.push(`ALPN=${record.alpn}`);
- if (record.path) parts.push(`path=${record.path}`);
- const condition = parts.length
- ? `${t('pages.inbounds.fallbacks.routesWhen') || 'Routes when'} ${parts.join(' · ')}`
- : (t('pages.inbounds.fallbacks.defaultCatchAll') || 'Default — catches anything else');
- const proxyTag = record.xver === 2 ? ' · PROXY v2' : record.xver === 1 ? ' · PROXY v1' : '';
- return { condition, proxyTag };
- }, [t]);
- const withSaving = useCallback(async <T,>(fn: () => Promise<T>): Promise<T> => {
- setSaving(true);
- try { return await fn(); } finally { setSaving(false); }
- }, []);
- const randomSSPassword = useCallback((target: ShadowsocksClient) => {
- if (target) {
- target.password = RandomUtil.randomShadowsocksPassword(inboundRef.current.settings.method);
- refresh();
- }
- }, [refresh]);
- const regenWgKeypair = useCallback((target: WireguardPeer) => {
- const kp = Wireguard.generateKeypair();
- target.publicKey = kp.publicKey;
- target.privateKey = kp.privateKey;
- refresh();
- }, [refresh]);
- const regenInboundWg = useCallback(() => {
- const kp = Wireguard.generateKeypair();
- inboundRef.current.settings.pubKey = kp.publicKey;
- inboundRef.current.settings.secretKey = kp.privateKey;
- refresh();
- }, [refresh]);
- const genRealityKeypair = useCallback(async () => {
- await withSaving(async () => {
- const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
- if (msg?.success) {
- const obj = msg.obj as { privateKey: string; publicKey: string };
- inboundRef.current.stream.reality.privateKey = obj.privateKey;
- inboundRef.current.stream.reality.settings.publicKey = obj.publicKey;
- refresh();
- }
- });
- }, [withSaving, refresh]);
- const clearRealityKeypair = useCallback(() => {
- if (!inboundRef.current?.stream?.reality) return;
- inboundRef.current.stream.reality.privateKey = '';
- inboundRef.current.stream.reality.settings.publicKey = '';
- refresh();
- }, [refresh]);
- const genMldsa65 = useCallback(async () => {
- await withSaving(async () => {
- const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
- if (msg?.success) {
- const obj = msg.obj as { seed: string; verify: string };
- inboundRef.current.stream.reality.mldsa65Seed = obj.seed;
- inboundRef.current.stream.reality.settings.mldsa65Verify = obj.verify;
- refresh();
- }
- });
- }, [withSaving, refresh]);
- const clearMldsa65 = useCallback(() => {
- if (!inboundRef.current?.stream?.reality) return;
- inboundRef.current.stream.reality.mldsa65Seed = '';
- inboundRef.current.stream.reality.settings.mldsa65Verify = '';
- refresh();
- }, [refresh]);
- const randomizeRealityTarget = useCallback(() => {
- if (!inboundRef.current?.stream?.reality) return;
- const target = getRandomRealityTarget() as { target: string; sni: string };
- inboundRef.current.stream.reality.target = target.target;
- inboundRef.current.stream.reality.serverNames = target.sni;
- refresh();
- }, [refresh]);
- const randomizeShortIds = useCallback(() => {
- if (!inboundRef.current?.stream?.reality) return;
- inboundRef.current.stream.reality.shortIds = RandomUtil.randomShortIds();
- refresh();
- }, [refresh]);
- const getNewEchCert = useCallback(async () => {
- if (!inboundRef.current?.stream?.tls) return;
- await withSaving(async () => {
- const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', {
- sni: inboundRef.current.stream.tls.sni,
- });
- if (msg?.success) {
- const obj = msg.obj as { echServerKeys: string; echConfigList: string };
- inboundRef.current.stream.tls.echServerKeys = obj.echServerKeys;
- inboundRef.current.stream.tls.settings.echConfigList = obj.echConfigList;
- refresh();
- }
- });
- }, [withSaving, refresh]);
- const clearEchCert = useCallback(() => {
- if (!inboundRef.current?.stream?.tls) return;
- inboundRef.current.stream.tls.echServerKeys = '';
- inboundRef.current.stream.tls.settings.echConfigList = '';
- refresh();
- }, [refresh]);
- const setDefaultCertData = useCallback((idx: number) => {
- if (!inboundRef.current?.stream?.tls?.certs?.[idx]) return;
- inboundRef.current.stream.tls.certs[idx].certFile = defaultCert;
- inboundRef.current.stream.tls.certs[idx].keyFile = defaultKey;
- refresh();
- }, [defaultCert, defaultKey, refresh]);
- const matchesVlessAuth = useCallback((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 = useCallback(async (authId: string) => {
- if (!authId || !inboundRef.current?.settings) return;
- await withSaving(async () => {
- 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;
- inboundRef.current.settings.decryption = block.decryption;
- inboundRef.current.settings.encryption = block.encryption;
- refresh();
- });
- }, [withSaving, refresh, matchesVlessAuth]);
- const clearVlessEnc = useCallback(() => {
- if (!inboundRef.current?.settings) return;
- inboundRef.current.settings.decryption = 'none';
- inboundRef.current.settings.encryption = 'none';
- refresh();
- }, [refresh]);
- const selectedVlessAuth = useMemo(() => {
- const encryption = inboundRef.current?.settings?.encryption;
- if (!encryption || encryption === 'none') return 'None';
- const parts = encryption.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');
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [inboundRef.current?.settings?.encryption, t]);
- const onSSMethodChange = useCallback(() => {
- const ib = inboundRef.current;
- ib.settings.password = RandomUtil.randomShadowsocksPassword(ib.settings.method);
- if (ib.isSSMultiUser) {
- ib.settings.shadowsockses.forEach((c: ShadowsocksClient) => {
- c.method = ib.isSS2022 ? '' : ib.settings.method;
- c.password = RandomUtil.randomShadowsocksPassword(ib.settings.method);
- });
- } else {
- ib.settings.shadowsockses = [];
- }
- refresh();
- }, [refresh]);
- const parseAdvancedSliceOrFallback = (rawText: string, fallback: unknown) => {
- if (!rawText?.trim()) return fallback;
- return JSON.parse(rawText);
- };
- const settingsFallback = () => inboundRef.current?.settings?.toJson?.() || {};
- const sniffingFallback = () => inboundRef.current?.sniffing?.toJson?.() || {};
- const streamFallback = () => inboundRef.current?.stream?.toJson?.() || {};
- const parseAdvancedSliceWithLabel = useCallback((rawText: string, fallback: unknown, label: string) => {
- try {
- return parseAdvancedSliceOrFallback(rawText, fallback);
- } catch (e) {
- messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
- throw e;
- }
- }, [messageApi]);
- const compactAdvancedJson = useCallback((raw: string, fallback: string, label: string) => {
- try {
- return JSON.stringify(JSON.parse(raw || fallback));
- } catch (e) {
- messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
- throw e;
- }
- }, [messageApi]);
- const applyAdvancedJsonToBasic = useCallback((): boolean => {
- const ib = inboundRef.current;
- if (!ib) return true;
- let settings: unknown;
- let streamSettings: unknown;
- let sniffing: unknown;
- try {
- settings = parseAdvancedSliceWithLabel(advancedTextRef.current.settings, settingsFallback(), t('pages.inbounds.advanced.settings'));
- streamSettings = parseAdvancedSliceWithLabel(advancedTextRef.current.stream, streamFallback(), t('pages.inbounds.advanced.stream'));
- sniffing = parseAdvancedSliceWithLabel(advancedTextRef.current.sniffing, sniffingFallback(), t('pages.inbounds.advanced.sniffing'));
- } catch {
- return false;
- }
- try {
- inboundRef.current = Inbound.fromJson({
- port: ib.port,
- listen: ib.listen,
- protocol: ib.protocol,
- settings,
- streamSettings,
- tag: ib.tag,
- sniffing,
- clientStats: ib.clientStats,
- });
- refresh();
- } catch (e) {
- messageApi.error(`${t('pages.inbounds.advanced.jsonErrorPrefix')}: ${(e as Error).message}`);
- return false;
- }
- return true;
- }, [t, refresh, parseAdvancedSliceWithLabel, messageApi]);
- const handleTabChange = (next: string) => {
- if (document.activeElement instanceof HTMLElement) {
- document.activeElement.blur();
- }
- if (activeTabKey === 'advanced' && next !== 'advanced') {
- if (!applyAdvancedJsonToBasic()) return;
- }
- setActiveTabKey(next);
- };
- const unwrapWrappedObject = (parsed: unknown, key: string): unknown => {
- if (
- parsed
- && typeof parsed === 'object'
- && !Array.isArray(parsed)
- && (parsed as Record<string, unknown>)[key] !== undefined
- ) {
- return (parsed as Record<string, unknown>)[key];
- }
- return parsed;
- };
- const wrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings'): string => {
- const ib = inboundRef.current;
- if (!ib) return '';
- try {
- const fb = slice === 'settings' ? settingsFallback() : slice === 'sniffing' ? sniffingFallback() : streamFallback();
- const value = parseAdvancedSliceOrFallback(advancedTextRef.current[slice], fb);
- return JSON.stringify({ [key]: value }, null, 2);
- } catch {
- return '';
- }
- };
- const setWrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings', label: string, next: string) => {
- let parsed: unknown;
- try {
- parsed = JSON.parse(next);
- } catch (e) {
- messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
- return;
- }
- const unwrapped = unwrapWrappedObject(parsed, key);
- if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
- messageApi.error(`${label} JSON must be an object or { ${key}: { ... } }.`);
- return;
- }
- try {
- advancedTextRef.current[slice] = JSON.stringify(unwrapped, null, 2);
- refresh();
- } catch (e) {
- messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
- }
- };
- const advancedAllValue = (() => {
- const ib = inboundRef.current;
- if (!ib) return '';
- try {
- const result: Record<string, unknown> = {
- listen: ib.listen,
- port: ib.port,
- protocol: ib.protocol,
- settings: parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback()),
- sniffing: parseAdvancedSliceOrFallback(advancedTextRef.current.sniffing, sniffingFallback()),
- tag: ib.tag,
- };
- if (canEnableStream) {
- result.streamSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.stream, streamFallback());
- }
- return JSON.stringify(result, null, 2);
- } catch {
- return '';
- }
- })();
- const setAdvancedAllValue = (next: string) => {
- let parsedRaw: unknown;
- try {
- parsedRaw = JSON.parse(next);
- } catch (e) {
- messageApi.error(`All JSON invalid: ${(e as Error).message}`);
- return;
- }
- if (!parsedRaw || typeof parsedRaw !== 'object' || Array.isArray(parsedRaw)) {
- messageApi.error('All JSON must be an inbound object.');
- return;
- }
- const parsed = parsedRaw as {
- listen?: string;
- port?: number | string;
- protocol?: string;
- tag?: string;
- settings?: unknown;
- sniffing?: unknown;
- streamSettings?: unknown;
- };
- const ib = inboundRef.current;
- try {
- if (typeof parsed.listen === 'string') ib.listen = parsed.listen;
- if (parsed.port !== undefined) {
- const port = Number(parsed.port);
- if (Number.isFinite(port)) ib.port = port;
- }
- if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
- ib.protocol = parsed.protocol;
- }
- if (typeof parsed.tag === 'string') ib.tag = parsed.tag;
- const existingSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback());
- advancedTextRef.current.settings = JSON.stringify(parsed.settings ?? existingSettings, null, 2);
- advancedTextRef.current.sniffing = JSON.stringify(parsed.sniffing ?? sniffingFallback(), null, 2);
- advancedTextRef.current.stream = canEnableStream
- ? JSON.stringify(parsed.streamSettings ?? streamFallback(), null, 2)
- : '{}';
- refresh();
- } catch (e) {
- messageApi.error(`All JSON invalid: ${(e as Error).message}`);
- }
- };
- const saveFallbacks = useCallback(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;
- }, [fallbacks]);
- const submit = useCallback(async () => {
- const ib = inboundRef.current;
- const form = dbFormRef.current;
- if (!ib || !form) return;
- setSaving(true);
- try {
- let streamSettings: string;
- let sniffing: string;
- let settings: string;
- try {
- streamSettings = canEnableStream
- ? compactAdvancedJson(advancedTextRef.current.stream, '', t('pages.inbounds.advanced.stream'))
- : (ib.stream?.sockopt
- ? JSON.stringify({ sockopt: ib.stream.sockopt.toJson() })
- : '');
- sniffing = compactAdvancedJson(advancedTextRef.current.sniffing, ib.sniffing.toString(), t('pages.inbounds.advanced.sniffing'));
- settings = compactAdvancedJson(advancedTextRef.current.settings, ib.settings.toString(), t('pages.inbounds.advanced.settings'));
- } catch { return; }
- const baseCheck = InboundFormSchema.safeParse({
- remark: form.remark ?? '',
- enable: !!form.enable,
- port: Number(ib.port),
- listen: ib.listen ?? '',
- protocol: ib.protocol ?? '',
- });
- if (!baseCheck.success) {
- const issue = baseCheck.error.issues[0];
- messageApi.error(t(issue?.message ?? 'somethingWentWrong', { defaultValue: issue?.message ?? 'invalid' }));
- return;
- }
- const payload: Record<string, unknown> = {
- up: form.up || 0,
- down: form.down || 0,
- total: form.total,
- remark: form.remark,
- enable: form.enable,
- expiryTime: form.expiryTime,
- trafficReset: form.trafficReset,
- lastTrafficResetTime: form.lastTrafficResetTime || 0,
- listen: ib.listen,
- port: ib.port,
- protocol: ib.protocol,
- settings,
- streamSettings,
- sniffing,
- };
- if (form.nodeId != null) payload.nodeId = form.nodeId;
- const url = mode === 'edit'
- ? `/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);
- if (masterId) await saveFallbacks(masterId);
- }
- onSaved();
- onClose();
- }
- } finally {
- setSaving(false);
- }
- }, [canEnableStream, compactAdvancedJson, t, messageApi, mode, dbInbound, isFallbackHost, saveFallbacks, onSaved, onClose]);
- const protocolSnapshot = inboundRef.current?.protocol;
- const streamSnapshot = JSON.stringify(inboundRef.current?.stream?.toJson?.() || {});
- const sniffingSnapshot = JSON.stringify(inboundRef.current?.sniffing?.toJson?.() || {});
- const settingsSnapshot = JSON.stringify(inboundRef.current?.settings?.toJson?.() || {});
- useEffect(() => {
- if (!inboundRef.current) return;
- (['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor);
- }, [protocolSnapshot, streamSnapshot, sniffingSnapshot, settingsSnapshot, stampAdvancedTextFor]);
- const title = mode === 'edit' ? t('pages.inbounds.modifyInbound') : t('pages.inbounds.addInbound');
- const okText = mode === 'edit' ? t('pages.clients.submitEdit') : t('create');
- const ib = inboundRef.current;
- const form = dbFormRef.current;
- if (!ib || !form) {
- return <Modal open={open} onCancel={onClose} title={title} footer={null} width={780} />;
- }
- const totalGB = form.total ? Math.round((form.total / SizeFormatter.ONE_GB) * 100) / 100 : 0;
- const expiryDate: Dayjs | null = form.expiryTime > 0 ? dayjs(form.expiryTime) : null;
- const renderBasicsTab = () => (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
- <Form.Item label={t('enable')}>
- <Switch checked={!!form.enable} onChange={(v) => { form.enable = v; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.remark')}>
- <Input value={form.remark} onChange={(e) => { form.remark = e.target.value; refresh(); }} />
- </Form.Item>
- {selectableNodes.length > 0 && isNodeEligible && (
- <Form.Item label={t('pages.inbounds.deployTo')}>
- <Select
- value={form.nodeId ?? ''}
- disabled={mode === 'edit'}
- placeholder={t('pages.inbounds.localPanel')}
- allowClear
- onChange={(v) => { form.nodeId = v === '' || v == null ? null : v; refresh(); }}
- >
- <Select.Option value="">{t('pages.inbounds.localPanel')}</Select.Option>
- {selectableNodes.map((n: NodeRecord) => (
- <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 label={t('pages.inbounds.protocol')}>
- <Select
- value={ib.protocol}
- disabled={mode === 'edit'}
- onChange={onProtocolChange}
- >
- {PROTOCOLS.map((p) => <Select.Option key={p} value={p}>{p}</Select.Option>)}
- </Select>
- </Form.Item>
- <Form.Item label={t('pages.inbounds.address')}>
- <Input
- value={ib.listen}
- placeholder={t('pages.inbounds.monitorDesc')}
- onChange={(e) => { ib.listen = e.target.value; refresh(); }}
- />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.port')}>
- <InputNumber
- value={ib.port}
- min={1}
- max={65535}
- onChange={(v) => { ib.port = Number(v) || 0; refresh(); }}
- />
- </Form.Item>
- <Form.Item label={<Tooltip title={t('pages.inbounds.meansNoLimit')}>{t('pages.inbounds.totalFlow')}</Tooltip>}>
- <InputNumber
- value={totalGB}
- min={0}
- step={1}
- onChange={(v) => {
- form.total = NumberFormatter.toFixed((Number(v) || 0) * SizeFormatter.ONE_GB, 0);
- refresh();
- }}
- />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.periodicTrafficResetTitle')}>
- <Select value={form.trafficReset} onChange={(v) => { form.trafficReset = v; refresh(); }}>
- {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>}>
- <DateTimePicker
- value={expiryDate}
- onChange={(d) => { form.expiryTime = d ? d.valueOf() : 0; refresh(); }}
- />
- </Form.Item>
- </Form>
- );
- const renderFallbacksCard = () => (
- <Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
- <Paragraph type="secondary" style={{ marginBottom: 12 }}>
- {t('pages.inbounds.fallbacks.help') || 'When a connection on this inbound does not match any client, route it to another inbound. Pick a child below and the routing fields (SNI / ALPN / path / xver) auto-fill from its transport — most setups need no further tweaking. Each child should listen on 127.0.0.1 with security=none.'}
- </Paragraph>
- {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, index) => (
- <div key={record.rowKey} style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}>
- <Row gutter={8} align="middle" wrap={false}>
- <Col flex="none">
- <Space orientation="vertical" size={2}>
- <Button size="small" type="text" disabled={index === 0} onClick={() => moveFallback(index, -1)}>
- <CaretUpOutlined />
- </Button>
- <Button size="small" type="text" disabled={index === fallbacks.length - 1} onClick={() => moveFallback(index, 1)}>
- <CaretDownOutlined />
- </Button>
- </Space>
- </Col>
- <Col flex="auto">
- <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) => onFallbackChildPicked(record.rowKey, v)}
- />
- <Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
- {describeFallback(record).condition}{describeFallback(record).proxyTag}
- </Text>
- </Col>
- <Col flex="none">
- <Space size={4}>
- <Tooltip title={t('pages.inbounds.fallbacks.rederive') || 'Re-fill from child'}>
- <Button size="small" type="text" disabled={!record.childId} onClick={() => rederiveFallback(record.rowKey)}>
- <SyncOutlined />
- </Button>
- </Tooltip>
- <Tooltip title={fallbackEditing.has(record.rowKey)
- ? (t('pages.inbounds.fallbacks.hideAdvanced') || 'Hide advanced')
- : (t('pages.inbounds.fallbacks.editAdvanced') || 'Edit routing fields')}>
- <Button size="small" type="text" onClick={() => toggleFallbackEdit(record.rowKey)}>
- <SettingOutlined />
- </Button>
- </Tooltip>
- <Button size="small" type="text" danger onClick={() => removeFallback(index)}>
- <DeleteOutlined />
- </Button>
- </Space>
- </Col>
- </Row>
- {fallbackEditing.has(record.rowKey) && (
- <Row gutter={8} style={{ marginTop: 8 }}>
- <Col xs={24} md={8}>
- <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 })} />
- </Space.Compact>
- </Col>
- <Col xs={24} md={5}>
- <Space.Compact block>
- <InputAddon>ALPN</InputAddon>
- <Input placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
- value={record.alpn} onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })} />
- </Space.Compact>
- </Col>
- <Col xs={24} md={7}>
- <Space.Compact block>
- <InputAddon>Path</InputAddon>
- <Input placeholder="/" value={record.path}
- onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })} />
- </Space.Compact>
- </Col>
- <Col xs={24} md={4}>
- <Space.Compact block>
- <InputAddon>xver</InputAddon>
- <InputNumber min={0} max={2} style={{ width: '100%' }}
- value={record.xver}
- onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })} />
- </Space.Compact>
- </Col>
- </Row>
- )}
- </div>
- ))}
- <Space size={8} style={{ marginTop: 4 }} wrap>
- <Button size="small" onClick={() => addFallback()}>
- <PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
- </Button>
- <Button size="small" type="primary" ghost onClick={quickAddAllFallbacks}>
- {t('pages.inbounds.fallbacks.quickAddAll') || 'Quick add all eligible'}
- </Button>
- </Space>
- </Card>
- );
- const renderProtocolTab = () => (
- <>
- {isVlessLike && (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
- <Form.Item label={t('pages.inbounds.decryption')}>
- <Input value={ib.settings.decryption} onChange={(e) => { ib.settings.decryption = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.encryption')}>
- <Input value={ib.settings.encryption} onChange={(e) => { ib.settings.encryption = e.target.value; refresh(); }} />
- </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>
- </Form>
- )}
- {isFallbackHost && renderFallbacksCard()}
- {ib.protocol === Protocols.SHADOWSOCKS && (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
- <Form.Item label="Encryption method">
- <Select value={ib.settings.method} onChange={(v) => { ib.settings.method = v; onSSMethodChange(); }}>
- {Object.entries(SSMethods).map(([k, m]) => (
- <Select.Option key={k} value={m as string}>{k}</Select.Option>
- ))}
- </Select>
- </Form.Item>
- {ib.isSS2022 && (
- <Form.Item label={<>Password <SyncOutlined className="random-icon" onClick={() => randomSSPassword(ib.settings)} /></>}>
- <Input value={ib.settings.password} onChange={(e) => { ib.settings.password = e.target.value; refresh(); }} />
- </Form.Item>
- )}
- <Form.Item label="Network">
- <Select value={ib.settings.network} style={{ width: 120 }} onChange={(v) => { ib.settings.network = v; refresh(); }}>
- <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 label="ivCheck">
- <Switch checked={!!ib.settings.ivCheck} onChange={(v) => { ib.settings.ivCheck = v; refresh(); }} />
- </Form.Item>
- </Form>
- )}
- {(ib.protocol === Protocols.HTTP || ib.protocol === Protocols.MIXED) && (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
- <Form.Item label="Accounts">
- <Button size="small" onClick={() => {
- const Account = ib.protocol === Protocols.HTTP
- ? Inbound.HttpSettings.HttpAccount
- : Inbound.MixedSettings.SocksAccount;
- ib.settings.addAccount(new Account());
- refresh();
- }}>
- <PlusOutlined /> Add
- </Button>
- </Form.Item>
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.settings.accounts || []).map((account: HttpAccount, idx: number) => (
- <Space.Compact key={idx} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={account.user} placeholder="Username"
- onChange={(e) => { account.user = e.target.value; refresh(); }} />
- <Input value={account.pass} placeholder="Password"
- onChange={(e) => { account.pass = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.settings.delAccount(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- {ib.protocol === Protocols.HTTP && (
- <Form.Item label="Allow transparent">
- <Switch checked={!!ib.settings.allowTransparent} onChange={(v) => { ib.settings.allowTransparent = v; refresh(); }} />
- </Form.Item>
- )}
- {ib.protocol === Protocols.MIXED && (
- <>
- <Form.Item label="Auth">
- <Select value={ib.settings.auth} onChange={(v) => { ib.settings.auth = v; refresh(); }}>
- <Select.Option value="noauth">noauth</Select.Option>
- <Select.Option value="password">password</Select.Option>
- </Select>
- </Form.Item>
- <Form.Item label="UDP">
- <Switch checked={!!ib.settings.udp} onChange={(v) => { ib.settings.udp = v; refresh(); }} />
- </Form.Item>
- {ib.settings.udp && (
- <Form.Item label="UDP IP">
- <Input value={ib.settings.ip} onChange={(e) => { ib.settings.ip = e.target.value; refresh(); }} />
- </Form.Item>
- )}
- </>
- )}
- </Form>
- )}
- {ib.protocol === Protocols.TUNNEL && (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
- <Form.Item label="Rewrite address">
- <Input value={ib.settings.rewriteAddress} onChange={(e) => { ib.settings.rewriteAddress = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="Rewrite port">
- <InputNumber value={ib.settings.rewritePort} min={0} max={65535}
- onChange={(v) => { ib.settings.rewritePort = Number(v) || 0; refresh(); }} />
- </Form.Item>
- <Form.Item label="Allowed network">
- <Select value={ib.settings.allowedNetwork} onChange={(v) => { ib.settings.allowedNetwork = v; refresh(); }}>
- <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 label="Port map">
- <Button size="small" onClick={() => { ib.settings.addPortMap('', ''); refresh(); }}>
- <PlusOutlined />
- </Button>
- </Form.Item>
- {(ib.settings.portMap || []).length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.settings.portMap as { name: string; value: string }[]).map((pm, idx) => (
- <Space.Compact key={`pm-${idx}`} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={pm.name} placeholder="5555"
- onChange={(e) => { pm.name = e.target.value; refresh(); }} />
- <Input value={pm.value} placeholder="1.1.1.1:7777"
- onChange={(e) => { pm.value = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.settings.removePortMap(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- <Form.Item label="Follow redirect">
- <Switch checked={!!ib.settings.followRedirect} onChange={(v) => { ib.settings.followRedirect = v; refresh(); }} />
- </Form.Item>
- </Form>
- )}
- {ib.protocol === Protocols.TUN && (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
- <Form.Item label="Interface name">
- <Input value={ib.settings.name} placeholder="xray0"
- onChange={(e) => { ib.settings.name = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="MTU">
- <InputNumber value={ib.settings.mtu} min={0}
- onChange={(v) => { ib.settings.mtu = Number(v) || 0; refresh(); }} />
- </Form.Item>
- <Form.Item label="Gateway">
- <Button size="small" onClick={() => { ib.settings.gateway.push(''); refresh(); }}>
- <PlusOutlined />
- </Button>
- {(ib.settings.gateway || []).map((_ip: string, j: number) => (
- <Space.Compact key={`tun-gw-${j}`} block className="mt-4">
- <Input
- placeholder={j === 0 ? '10.0.0.1/16' : 'fc00::1/64'}
- value={ib.settings.gateway[j]}
- onChange={(e) => { ib.settings.gateway[j] = e.target.value; refresh(); }} />
- <Button size="small" onClick={() => { ib.settings.gateway.splice(j, 1); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- <Form.Item label="DNS">
- <Button size="small" onClick={() => { ib.settings.dns.push(''); refresh(); }}>
- <PlusOutlined />
- </Button>
- {(ib.settings.dns || []).map((_ip: string, j: number) => (
- <Space.Compact key={`tun-dns-${j}`} block className="mt-4">
- <Input
- placeholder={j === 0 ? '1.1.1.1' : '8.8.8.8'}
- value={ib.settings.dns[j]}
- onChange={(e) => { ib.settings.dns[j] = e.target.value; refresh(); }} />
- <Button size="small" onClick={() => { ib.settings.dns.splice(j, 1); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- <Form.Item label="User level">
- <InputNumber value={ib.settings.userLevel} min={0}
- onChange={(v) => { ib.settings.userLevel = Number(v) || 0; refresh(); }} />
- </Form.Item>
- <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={() => { ib.settings.autoSystemRoutingTable.push(''); refresh(); }}>
- <PlusOutlined />
- </Button>
- {(ib.settings.autoSystemRoutingTable || []).map((_ip: string, j: number) => (
- <Space.Compact key={`tun-rt-${j}`} block className="mt-4">
- <Input
- placeholder={j === 0 ? '0.0.0.0/0' : '::/0'}
- value={ib.settings.autoSystemRoutingTable[j]}
- onChange={(e) => { ib.settings.autoSystemRoutingTable[j] = e.target.value; refresh(); }} />
- <Button size="small" onClick={() => { ib.settings.autoSystemRoutingTable.splice(j, 1); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- <Form.Item 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 value={ib.settings.autoOutboundsInterface} placeholder="auto"
- onChange={(e) => { ib.settings.autoOutboundsInterface = e.target.value; refresh(); }} />
- </Form.Item>
- </Form>
- )}
- {ib.protocol === Protocols.WIREGUARD && (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
- <Form.Item label={<>Secret key <SyncOutlined className="random-icon" onClick={regenInboundWg} /></>}>
- <Input value={ib.settings.secretKey}
- onChange={(e) => { ib.settings.secretKey = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="Public key">
- <Input value={ib.settings.pubKey} disabled />
- </Form.Item>
- <Form.Item label="MTU">
- <InputNumber value={ib.settings.mtu}
- onChange={(v) => { ib.settings.mtu = Number(v) || 0; refresh(); }} />
- </Form.Item>
- <Form.Item label="No-kernel TUN">
- <Switch checked={!!ib.settings.noKernelTun}
- onChange={(v) => { ib.settings.noKernelTun = v; refresh(); }} />
- </Form.Item>
- <Form.Item label="Peers">
- <Button size="small" onClick={() => { ib.settings.addPeer(); refresh(); }}>
- <PlusOutlined /> Add peer
- </Button>
- </Form.Item>
- {(ib.settings.peers || []).map((peer: WireguardPeer, idx: number) => (
- <div key={idx} className="wg-peer">
- <Divider style={{ margin: '8px 0' }}>
- Peer {idx + 1}
- {ib.settings.peers.length > 1 && (
- <DeleteOutlined className="danger-icon" onClick={() => { ib.settings.delPeer(idx); refresh(); }} />
- )}
- </Divider>
- <Form.Item label={<>Secret key <SyncOutlined className="random-icon" onClick={() => regenWgKeypair(peer)} /></>}>
- <Input value={peer.privateKey} onChange={(e) => { peer.privateKey = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="Public key">
- <Input value={peer.publicKey} onChange={(e) => { peer.publicKey = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="PSK">
- <Input value={peer.psk} onChange={(e) => { peer.psk = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="Allowed IPs">
- <Button size="small" onClick={() => { peer.allowedIPs.push(''); refresh(); }}>
- <PlusOutlined />
- </Button>
- {(peer.allowedIPs || []).map((_ip: string, j: number) => (
- <Space.Compact key={j} block className="mt-4">
- <Input
- value={peer.allowedIPs[j]}
- onChange={(e) => { peer.allowedIPs[j] = e.target.value; refresh(); }} />
- {peer.allowedIPs.length > 1 && (
- <Button size="small" onClick={() => { peer.allowedIPs.splice(j, 1); refresh(); }}>
- <MinusOutlined />
- </Button>
- )}
- </Space.Compact>
- ))}
- </Form.Item>
- <Form.Item label="Keep-alive">
- <InputNumber value={peer.keepAlive} min={0}
- onChange={(v) => { peer.keepAlive = Number(v) || 0; refresh(); }} />
- </Form.Item>
- </div>
- ))}
- </Form>
- )}
- </>
- );
- const renderStreamTab = () => {
- const network = ib.stream?.network;
- return (
- <>
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
- {ib.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' && (
- <>
- {canEnableTls && (
- <Form.Item label="Proxy Protocol">
- <Switch checked={!!ib.stream.tcp.acceptProxyProtocol}
- onChange={(v) => { ib.stream.tcp.acceptProxyProtocol = v; refresh(); }} />
- </Form.Item>
- )}
- <Form.Item label={`HTTP ${t('camouflage')}`}>
- <Switch checked={ib.stream.tcp.type === 'http'}
- onChange={(v) => { ib.stream.tcp.type = v ? 'http' : 'none'; refresh(); }} />
- </Form.Item>
- {ib.stream.tcp.type === 'http' && (
- <>
- <Divider style={{ margin: 0 }}>{t('pages.inbounds.stream.general.request')}</Divider>
- <Form.Item label={t('pages.inbounds.stream.tcp.version')}>
- <Input value={ib.stream.tcp.request.version}
- onChange={(e) => { ib.stream.tcp.request.version = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.method')}>
- <Input value={ib.stream.tcp.request.method}
- onChange={(e) => { ib.stream.tcp.request.method = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={<>{t('pages.inbounds.stream.tcp.path')} <Button size="small" style={{ marginLeft: 6 }} onClick={() => { ib.stream.tcp.request.addPath('/'); refresh(); }}><PlusOutlined /></Button></>}>
- {(ib.stream.tcp.request.path || []).map((_p: string, idx: number) => (
- <Space.Compact key={`tcp-path-${idx}`} block className="mb-4">
- <Input
- value={ib.stream.tcp.request.path[idx]}
- onChange={(e) => { ib.stream.tcp.request.path[idx] = e.target.value; refresh(); }} />
- {ib.stream.tcp.request.path.length > 1 && (
- <Button size="small" onClick={() => { ib.stream.tcp.request.removePath(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- )}
- </Space.Compact>
- ))}
- </Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
- <Button size="small" onClick={() => { ib.stream.tcp.request.addHeader('Host', ''); refresh(); }}>
- <PlusOutlined />
- </Button>
- </Form.Item>
- {(ib.stream.tcp.request.headers || []).length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.stream.tcp.request.headers as { name: string; value: string }[]).map((h, idx) => (
- <Space.Compact key={`tcp-rh-${idx}`} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={h.name}
- placeholder={t('pages.inbounds.stream.general.name')}
- onChange={(e) => { h.name = e.target.value; refresh(); }} />
- <Input value={h.value}
- placeholder={t('pages.inbounds.stream.general.value')}
- onChange={(e) => { h.value = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.stream.tcp.request.removeHeader(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- <Divider style={{ margin: 0 }}>{t('pages.inbounds.stream.general.response')}</Divider>
- <Form.Item label={t('pages.inbounds.stream.tcp.version')}>
- <Input value={ib.stream.tcp.response.version}
- onChange={(e) => { ib.stream.tcp.response.version = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.status')}>
- <Input value={ib.stream.tcp.response.status}
- onChange={(e) => { ib.stream.tcp.response.status = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.statusDescription')}>
- <Input value={ib.stream.tcp.response.reason}
- onChange={(e) => { ib.stream.tcp.response.reason = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.responseHeader')}>
- <Button size="small" onClick={() => { ib.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream'); refresh(); }}>
- <PlusOutlined />
- </Button>
- </Form.Item>
- {(ib.stream.tcp.response.headers || []).length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.stream.tcp.response.headers as { name: string; value: string }[]).map((h, idx) => (
- <Space.Compact key={`tcp-rsh-${idx}`} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={h.name}
- placeholder={t('pages.inbounds.stream.general.name')}
- onChange={(e) => { h.name = e.target.value; refresh(); }} />
- <Input value={h.value}
- placeholder={t('pages.inbounds.stream.general.value')}
- onChange={(e) => { h.value = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.stream.tcp.response.removeHeader(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </>
- )}
- </>
- )}
- {network === 'kcp' && (
- <>
- <Form.Item label="MTU"><InputNumber value={ib.stream.kcp.mtu} min={576} max={1460} onChange={(v) => { ib.stream.kcp.mtu = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="TTI (ms)"><InputNumber value={ib.stream.kcp.tti} min={10} max={100} onChange={(v) => { ib.stream.kcp.tti = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Uplink (MB/s)"><InputNumber value={ib.stream.kcp.upCap} min={0} onChange={(v) => { ib.stream.kcp.upCap = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Downlink (MB/s)"><InputNumber value={ib.stream.kcp.downCap} min={0} onChange={(v) => { ib.stream.kcp.downCap = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="CWND Multiplier"><InputNumber value={ib.stream.kcp.cwndMultiplier} min={1} onChange={(v) => { ib.stream.kcp.cwndMultiplier = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Max Sending Window"><InputNumber value={ib.stream.kcp.maxSendingWindow} min={0} onChange={(v) => { ib.stream.kcp.maxSendingWindow = Number(v) || 0; refresh(); }} /></Form.Item>
- </>
- )}
- {network === 'ws' && (
- <>
- <Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.ws.acceptProxyProtocol} onChange={(v) => { ib.stream.ws.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
- <Form.Item label={t('host')}><Input value={ib.stream.ws.host} onChange={(e) => { ib.stream.ws.host = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label={t('path')}><Input value={ib.stream.ws.path} onChange={(e) => { ib.stream.ws.path = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Heartbeat Period"><InputNumber value={ib.stream.ws.heartbeatPeriod} min={0} onChange={(v) => { ib.stream.ws.heartbeatPeriod = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
- <Button size="small" onClick={() => { ib.stream.ws.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
- </Form.Item>
- {(ib.stream.ws.headers || []).length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.stream.ws.headers as { name: string; value: string }[]).map((h, idx) => (
- <Space.Compact key={`ws-h-${idx}`} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={h.name}
- placeholder={t('pages.inbounds.stream.general.name')}
- onChange={(e) => { h.name = e.target.value; refresh(); }} />
- <Input value={h.value}
- placeholder={t('pages.inbounds.stream.general.value')}
- onChange={(e) => { h.value = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.stream.ws.removeHeader(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </>
- )}
- {network === 'grpc' && (
- <>
- <Form.Item label="Service Name"><Input value={ib.stream.grpc.serviceName} onChange={(e) => { ib.stream.grpc.serviceName = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Authority"><Input value={ib.stream.grpc.authority} onChange={(e) => { ib.stream.grpc.authority = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Multi Mode"><Switch checked={!!ib.stream.grpc.multiMode} onChange={(v) => { ib.stream.grpc.multiMode = v; refresh(); }} /></Form.Item>
- </>
- )}
- {network === 'httpupgrade' && (
- <>
- <Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.httpupgrade.acceptProxyProtocol} onChange={(v) => { ib.stream.httpupgrade.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
- <Form.Item label={t('host')}><Input value={ib.stream.httpupgrade.host} onChange={(e) => { ib.stream.httpupgrade.host = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label={t('path')}><Input value={ib.stream.httpupgrade.path} onChange={(e) => { ib.stream.httpupgrade.path = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
- <Button size="small" onClick={() => { ib.stream.httpupgrade.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
- </Form.Item>
- {(ib.stream.httpupgrade.headers || []).length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.stream.httpupgrade.headers as { name: string; value: string }[]).map((h, idx) => (
- <Space.Compact key={`hu-h-${idx}`} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={h.name}
- placeholder={t('pages.inbounds.stream.general.name')}
- onChange={(e) => { h.name = e.target.value; refresh(); }} />
- <Input value={h.value}
- placeholder={t('pages.inbounds.stream.general.value')}
- onChange={(e) => { h.value = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.stream.httpupgrade.removeHeader(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </>
- )}
- {network === 'xhttp' && (
- <>
- <Form.Item label={t('host')}><Input value={ib.stream.xhttp.host} onChange={(e) => { ib.stream.xhttp.host = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label={t('path')}><Input value={ib.stream.xhttp.path} onChange={(e) => { ib.stream.xhttp.path = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
- <Button size="small" onClick={() => { ib.stream.xhttp.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
- </Form.Item>
- {(ib.stream.xhttp.headers || []).length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.stream.xhttp.headers as { name: string; value: string }[]).map((h, idx) => (
- <Space.Compact key={`xh-h-${idx}`} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={h.name}
- placeholder={t('pages.inbounds.stream.general.name')}
- onChange={(e) => { h.name = e.target.value; refresh(); }} />
- <Input value={h.value}
- placeholder={t('pages.inbounds.stream.general.value')}
- onChange={(e) => { h.value = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.stream.xhttp.removeHeader(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- <Form.Item label="Mode">
- <Select value={ib.stream.xhttp.mode} style={{ width: '50%' }} onChange={(v) => { ib.stream.xhttp.mode = v; refresh(); }}>
- {MODE_OPTIONS.map((m) => <Select.Option key={m} value={m}>{m}</Select.Option>)}
- </Select>
- </Form.Item>
- {ib.stream.xhttp.mode === 'packet-up' && (
- <>
- <Form.Item label="Max Buffered Upload"><InputNumber value={ib.stream.xhttp.scMaxBufferedPosts} onChange={(v) => { ib.stream.xhttp.scMaxBufferedPosts = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Max Upload Size (Byte)"><Input value={ib.stream.xhttp.scMaxEachPostBytes} onChange={(e) => { ib.stream.xhttp.scMaxEachPostBytes = e.target.value; refresh(); }} /></Form.Item>
- </>
- )}
- {ib.stream.xhttp.mode === 'stream-up' && (
- <Form.Item label="Stream-Up Server"><Input value={ib.stream.xhttp.scStreamUpServerSecs} onChange={(e) => { ib.stream.xhttp.scStreamUpServerSecs = e.target.value; refresh(); }} /></Form.Item>
- )}
- <Form.Item label="Server Max Header Bytes"><InputNumber value={ib.stream.xhttp.serverMaxHeaderBytes} min={0} placeholder="0 (default)" onChange={(v) => { ib.stream.xhttp.serverMaxHeaderBytes = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Padding Bytes"><Input value={ib.stream.xhttp.xPaddingBytes} onChange={(e) => { ib.stream.xhttp.xPaddingBytes = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Uplink HTTP Method">
- <Select value={ib.stream.xhttp.uplinkHTTPMethod || ''} onChange={(v) => { ib.stream.xhttp.uplinkHTTPMethod = v; refresh(); }}>
- <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={ib.stream.xhttp.mode !== 'packet-up'}>GET (packet-up only)</Select.Option>
- </Select>
- </Form.Item>
- <Form.Item label="Padding Obfs Mode"><Switch checked={!!ib.stream.xhttp.xPaddingObfsMode} onChange={(v) => { ib.stream.xhttp.xPaddingObfsMode = v; refresh(); }} /></Form.Item>
- {ib.stream.xhttp.xPaddingObfsMode && (
- <>
- <Form.Item label="Padding Key"><Input value={ib.stream.xhttp.xPaddingKey} placeholder="x_padding" onChange={(e) => { ib.stream.xhttp.xPaddingKey = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Padding Header"><Input value={ib.stream.xhttp.xPaddingHeader} placeholder="X-Padding" onChange={(e) => { ib.stream.xhttp.xPaddingHeader = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Padding Placement">
- <Select value={ib.stream.xhttp.xPaddingPlacement} onChange={(v) => { ib.stream.xhttp.xPaddingPlacement = v; refresh(); }}>
- <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 label="Padding Method">
- <Select value={ib.stream.xhttp.xPaddingMethod} onChange={(v) => { ib.stream.xhttp.xPaddingMethod = v; refresh(); }}>
- <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 label="Session Placement">
- <Select value={ib.stream.xhttp.sessionPlacement} onChange={(v) => { ib.stream.xhttp.sessionPlacement = v; refresh(); }}>
- <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>
- {ib.stream.xhttp.sessionPlacement && ib.stream.xhttp.sessionPlacement !== 'path' && (
- <Form.Item label="Session Key"><Input value={ib.stream.xhttp.sessionKey} placeholder="x_session" onChange={(e) => { ib.stream.xhttp.sessionKey = e.target.value; refresh(); }} /></Form.Item>
- )}
- <Form.Item label="Sequence Placement">
- <Select value={ib.stream.xhttp.seqPlacement} onChange={(v) => { ib.stream.xhttp.seqPlacement = v; refresh(); }}>
- <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>
- {ib.stream.xhttp.seqPlacement && ib.stream.xhttp.seqPlacement !== 'path' && (
- <Form.Item label="Sequence Key"><Input value={ib.stream.xhttp.seqKey} placeholder="x_seq" onChange={(e) => { ib.stream.xhttp.seqKey = e.target.value; refresh(); }} /></Form.Item>
- )}
- {ib.stream.xhttp.mode === 'packet-up' && (
- <Form.Item label="Uplink Data Placement">
- <Select value={ib.stream.xhttp.uplinkDataPlacement} onChange={(v) => { ib.stream.xhttp.uplinkDataPlacement = v; refresh(); }}>
- <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>
- )}
- {ib.stream.xhttp.mode === 'packet-up' && ib.stream.xhttp.uplinkDataPlacement && ib.stream.xhttp.uplinkDataPlacement !== 'body' && (
- <Form.Item label="Uplink Data Key"><Input value={ib.stream.xhttp.uplinkDataKey} placeholder="x_data" onChange={(e) => { ib.stream.xhttp.uplinkDataKey = e.target.value; refresh(); }} /></Form.Item>
- )}
- <Form.Item label="No SSE Header"><Switch checked={!!ib.stream.xhttp.noSSEHeader} onChange={(v) => { ib.stream.xhttp.noSSEHeader = v; refresh(); }} /></Form.Item>
- </>
- )}
- <Form.Item label="External Proxy">
- <Switch checked={externalProxyOn} onChange={setExternalProxy} />
- {externalProxyOn && (
- <Button size="small" type="primary" style={{ marginLeft: 10 }}
- onClick={() => { ib.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '', sni: '', fingerprint: '', alpn: [] }); refresh(); }}>
- <PlusOutlined />
- </Button>
- )}
- </Form.Item>
- {externalProxyOn && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string; sni?: string; fingerprint?: string; alpn?: string[] }[]).map((row, idx) => (
- <div key={`ep-${idx}`} style={{ margin: '8px 0' }}>
- <Space.Compact block>
- <Tooltip title="Force TLS">
- <Select value={row.forceTls} style={{ width: '20%' }} onChange={(v) => { row.forceTls = v; refresh(); }}>
- <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>
- </Tooltip>
- <Input style={{ width: '30%' }} value={row.dest} placeholder={t('host')}
- onChange={(e) => { row.dest = e.target.value; refresh(); }} />
- <Tooltip title={t('pages.inbounds.port')}>
- <InputNumber value={row.port} style={{ width: '15%' }} min={1} max={65535}
- onChange={(v) => { row.port = Number(v) || 0; refresh(); }} />
- </Tooltip>
- <Input style={{ width: '25%' }} value={row.remark} placeholder={t('pages.inbounds.remark')}
- onChange={(e) => { row.remark = e.target.value; refresh(); }} />
- <InputAddon onClick={() => { ib.stream.externalProxy.splice(idx, 1); refresh(); }}>
- <MinusOutlined />
- </InputAddon>
- </Space.Compact>
- {row.forceTls === 'tls' && (
- <Space.Compact style={{ marginTop: 6 }} block>
- <Input style={{ width: '30%' }} value={row.sni || ''} placeholder="SNI (defaults to host)"
- onChange={(e) => { row.sni = e.target.value; refresh(); }} />
- <Select value={row.fingerprint || ''} style={{ width: '30%' }} placeholder="Fingerprint"
- onChange={(v) => { row.fingerprint = v; refresh(); }}>
- <Select.Option value="">Default</Select.Option>
- {FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
- </Select>
- <Select mode="multiple" value={row.alpn || []} style={{ width: '40%' }} placeholder="ALPN"
- onChange={(v) => { row.alpn = v; refresh(); }}>
- {ALPNS.map((alpn) => <Select.Option key={alpn} value={alpn}>{alpn}</Select.Option>)}
- </Select>
- </Space.Compact>
- )}
- </div>
- ))}
- </Form.Item>
- )}
- <Form.Item label="Sockopt"><Switch checked={!!ib.stream.sockoptSwitch} onChange={(v) => { ib.stream.sockoptSwitch = v; refresh(); }} /></Form.Item>
- {ib.stream.sockoptSwitch && ib.stream.sockopt && (
- <>
- <Form.Item label="Route Mark"><InputNumber value={ib.stream.sockopt.mark} min={0} onChange={(v) => { ib.stream.sockopt.mark = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="TCP Keep Alive Interval"><InputNumber value={ib.stream.sockopt.tcpKeepAliveInterval} min={0} onChange={(v) => { ib.stream.sockopt.tcpKeepAliveInterval = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="TCP Keep Alive Idle"><InputNumber value={ib.stream.sockopt.tcpKeepAliveIdle} min={0} onChange={(v) => { ib.stream.sockopt.tcpKeepAliveIdle = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="TCP Max Seg"><InputNumber value={ib.stream.sockopt.tcpMaxSeg} min={0} onChange={(v) => { ib.stream.sockopt.tcpMaxSeg = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="TCP User Timeout"><InputNumber value={ib.stream.sockopt.tcpUserTimeout} min={0} onChange={(v) => { ib.stream.sockopt.tcpUserTimeout = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="TCP Window Clamp"><InputNumber value={ib.stream.sockopt.tcpWindowClamp} min={0} onChange={(v) => { ib.stream.sockopt.tcpWindowClamp = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.sockopt.acceptProxyProtocol} onChange={(v) => { ib.stream.sockopt.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
- <Form.Item label="TCP Fast Open"><Switch checked={!!ib.stream.sockopt.tcpFastOpen} onChange={(v) => { ib.stream.sockopt.tcpFastOpen = v; refresh(); }} /></Form.Item>
- <Form.Item label="Multipath TCP"><Switch checked={!!ib.stream.sockopt.tcpMptcp} onChange={(v) => { ib.stream.sockopt.tcpMptcp = v; refresh(); }} /></Form.Item>
- <Form.Item label="Penetrate"><Switch checked={!!ib.stream.sockopt.penetrate} onChange={(v) => { ib.stream.sockopt.penetrate = v; refresh(); }} /></Form.Item>
- <Form.Item label="V6 Only"><Switch checked={!!ib.stream.sockopt.V6Only} onChange={(v) => { ib.stream.sockopt.V6Only = v; refresh(); }} /></Form.Item>
- <Form.Item label="Domain Strategy">
- <Select value={ib.stream.sockopt.domainStrategy} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.domainStrategy = v; refresh(); }}>
- {DOMAIN_STRATEGIES.map((d) => <Select.Option key={d} value={d}>{d}</Select.Option>)}
- </Select>
- </Form.Item>
- <Form.Item label="TCP Congestion">
- <Select value={ib.stream.sockopt.tcpcongestion} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.tcpcongestion = v; refresh(); }}>
- {TCP_CONGESTIONS.map((c) => <Select.Option key={c} value={c}>{c}</Select.Option>)}
- </Select>
- </Form.Item>
- <Form.Item label="TProxy">
- <Select value={ib.stream.sockopt.tproxy} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.tproxy = v; refresh(); }}>
- <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 label="Dialer Proxy"><Input value={ib.stream.sockopt.dialerProxy} onChange={(e) => { ib.stream.sockopt.dialerProxy = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Interface Name"><Input value={ib.stream.sockopt.interfaceName} onChange={(e) => { ib.stream.sockopt.interfaceName = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Trusted X-Forwarded-For">
- <Select mode="tags" value={ib.stream.sockopt.trustedXForwardedFor} style={{ width: '100%' }}
- tokenSeparators={[',']}
- onChange={(v) => { ib.stream.sockopt.trustedXForwardedFor = v; refresh(); }}>
- <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>
- </>
- )}
- {ib.protocol === Protocols.HYSTERIA && (
- <>
- <Form.Item label={<Tooltip title="Hysteria protocol version. Currently must be 2.">Version</Tooltip>}>
- <InputNumber value={ib.stream.hysteria.version} min={2} max={2} onChange={(v) => { ib.stream.hysteria.version = Number(v) || 2; refresh(); }} />
- </Form.Item>
- <Form.Item label={<Tooltip title="Idle timeout (seconds) for a single QUIC native UDP connection.">UDP idle timeout</Tooltip>}>
- <InputNumber value={ib.stream.hysteria.udpIdleTimeout} min={0} onChange={(v) => { ib.stream.hysteria.udpIdleTimeout = Number(v) || 0; refresh(); }} />
- </Form.Item>
- <Form.Item label="Masquerade">
- <Switch checked={!!ib.stream.hysteria.masqueradeSwitch} onChange={(v) => { ib.stream.hysteria.masqueradeSwitch = v; refresh(); }} />
- </Form.Item>
- {ib.stream.hysteria.masqueradeSwitch && (
- <>
- <Form.Item label="Type">
- <Select value={ib.stream.hysteria.masquerade.type} style={{ width: '50%' }} onChange={(v) => { ib.stream.hysteria.masquerade.type = v; refresh(); }}>
- <Select.Option value="proxy">Proxy</Select.Option>
- <Select.Option value="file">File</Select.Option>
- <Select.Option value="string">String</Select.Option>
- </Select>
- </Form.Item>
- {ib.stream.hysteria.masquerade.type === 'proxy' && (
- <>
- <Form.Item label="URL"><Input value={ib.stream.hysteria.masquerade.url} placeholder="https://example.com" onChange={(e) => { ib.stream.hysteria.masquerade.url = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Rewrite Host"><Switch checked={!!ib.stream.hysteria.masquerade.rewriteHost} onChange={(v) => { ib.stream.hysteria.masquerade.rewriteHost = v; refresh(); }} /></Form.Item>
- <Form.Item label="Insecure"><Switch checked={!!ib.stream.hysteria.masquerade.insecure} onChange={(v) => { ib.stream.hysteria.masquerade.insecure = v; refresh(); }} /></Form.Item>
- </>
- )}
- {ib.stream.hysteria.masquerade.type === 'file' && (
- <Form.Item label="Directory"><Input value={ib.stream.hysteria.masquerade.dir} placeholder="/path/to/www" onChange={(e) => { ib.stream.hysteria.masquerade.dir = e.target.value; refresh(); }} /></Form.Item>
- )}
- {ib.stream.hysteria.masquerade.type === 'string' && (
- <>
- <Form.Item label="Content"><TextArea value={ib.stream.hysteria.masquerade.content} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.hysteria.masquerade.content = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Status Code"><InputNumber value={ib.stream.hysteria.masquerade.statusCode} min={100} max={599} placeholder="200" onChange={(v) => { ib.stream.hysteria.masquerade.statusCode = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Headers">
- <Button size="small" onClick={() => { ib.stream.hysteria.masquerade.addHeader('', ''); refresh(); }}>
- <PlusOutlined />
- </Button>
- </Form.Item>
- {(ib.stream.hysteria.masquerade.headers || []).length > 0 && (
- <Form.Item wrapperCol={{ span: 24 }}>
- {(ib.stream.hysteria.masquerade.headers as { name: string; value: string }[]).map((h, idx) => (
- <Space.Compact key={`mh-${idx}`} className="mb-8" block>
- <InputAddon>{String(idx + 1)}</InputAddon>
- <Input value={h.name} placeholder="Name"
- onChange={(e) => { h.name = e.target.value; refresh(); }} />
- <Input value={h.value} placeholder="Value"
- onChange={(e) => { h.value = e.target.value; refresh(); }} />
- <Button onClick={() => { ib.stream.hysteria.masquerade.removeHeader(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- </Space.Compact>
- ))}
- </Form.Item>
- )}
- </>
- )}
- </>
- )}
- </>
- )}
- </Form>
- <FinalMaskForm stream={ib.stream} protocol={ib.protocol} onChange={refresh} />
- </>
- );
- };
- const renderSecurityTab = () => (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
- <Form.Item label={t('pages.inbounds.securityTab')}>
- <Radio.Group value={ib.stream.security} buttonStyle="solid" disabled={!canEnableTls}
- onChange={(e) => setSecurity(e.target.value)}>
- <Radio.Button value="none">none</Radio.Button>
- <Radio.Button value="tls">tls</Radio.Button>
- {canEnableReality && <Radio.Button value="reality">reality</Radio.Button>}
- </Radio.Group>
- </Form.Item>
- {ib.stream.security === 'tls' && ib.stream.tls && (
- <>
- <Form.Item label="SNI"><Input value={ib.stream.tls.sni} placeholder="Server Name Indication" onChange={(e) => { ib.stream.tls.sni = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Cipher Suites">
- <Select value={ib.stream.tls.cipherSuites} onChange={(v) => { ib.stream.tls.cipherSuites = v; refresh(); }}>
- <Select.Option value="">Auto</Select.Option>
- {CIPHER_SUITES.map(([label, val]) => <Select.Option key={val} value={val}>{label}</Select.Option>)}
- </Select>
- </Form.Item>
- <Form.Item label="Min/Max Version">
- <Space.Compact block>
- <Select value={ib.stream.tls.minVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.minVersion = v; refresh(); }}>
- {TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
- </Select>
- <Select value={ib.stream.tls.maxVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.maxVersion = v; refresh(); }}>
- {TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
- </Select>
- </Space.Compact>
- </Form.Item>
- <Form.Item label="uTLS">
- <Select value={ib.stream.tls.settings.fingerprint} style={{ width: '100%' }} onChange={(v) => { ib.stream.tls.settings.fingerprint = v; refresh(); }}>
- <Select.Option value="">None</Select.Option>
- {FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
- </Select>
- </Form.Item>
- <Form.Item label="ALPN">
- <Select mode="multiple" value={ib.stream.tls.alpn} style={{ width: '100%' }} tokenSeparators={[',']}
- onChange={(v) => { ib.stream.tls.alpn = v; refresh(); }}>
- {ALPNS.map((a) => <Select.Option key={a} value={a}>{a}</Select.Option>)}
- </Select>
- </Form.Item>
- <Form.Item label="Reject Unknown SNI"><Switch checked={!!ib.stream.tls.rejectUnknownSni} onChange={(v) => { ib.stream.tls.rejectUnknownSni = v; refresh(); }} /></Form.Item>
- <Form.Item label="Disable System Root"><Switch checked={!!ib.stream.tls.disableSystemRoot} onChange={(v) => { ib.stream.tls.disableSystemRoot = v; refresh(); }} /></Form.Item>
- <Form.Item label="Session Resumption"><Switch checked={!!ib.stream.tls.enableSessionResumption} onChange={(v) => { ib.stream.tls.enableSessionResumption = v; refresh(); }} /></Form.Item>
- {(ib.stream.tls.certs || []).map((cert: TlsCert, idx: number) => (
- <div key={`cert-${idx}`}>
- <Form.Item label={t('certificate')}>
- <Radio.Group value={cert.useFile} buttonStyle="solid" onChange={(e) => { cert.useFile = e.target.value; refresh(); }}>
- <Radio.Button value={true}>{t('pages.inbounds.certificatePath')}</Radio.Button>
- <Radio.Button value={false}>{t('pages.inbounds.certificateContent')}</Radio.Button>
- </Radio.Group>
- </Form.Item>
- <Form.Item label=" ">
- <Space>
- {idx === 0 && (
- <Button type="primary" size="small" onClick={() => { ib.stream.tls.addCert(); refresh(); }}>
- <PlusOutlined />
- </Button>
- )}
- {ib.stream.tls.certs.length > 1 && (
- <Button type="primary" size="small" onClick={() => { ib.stream.tls.removeCert(idx); refresh(); }}>
- <MinusOutlined />
- </Button>
- )}
- </Space>
- </Form.Item>
- {cert.useFile ? (
- <>
- <Form.Item label={t('pages.inbounds.publicKey')}>
- <Input value={cert.certFile} onChange={(e) => { cert.certFile = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.privatekey')}>
- <Input value={cert.keyFile} onChange={(e) => { cert.keyFile = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label=" ">
- <Button type="primary" disabled={!defaultCert && !defaultKey} onClick={() => setDefaultCertData(idx)}>
- {t('pages.inbounds.setDefaultCert')}
- </Button>
- </Form.Item>
- </>
- ) : (
- <>
- <Form.Item label={t('pages.inbounds.publicKey')}>
- <TextArea value={cert.cert} autoSize={{ minRows: 3, maxRows: 8 }}
- onChange={(e) => { cert.cert = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.privatekey')}>
- <TextArea value={cert.key} autoSize={{ minRows: 3, maxRows: 8 }}
- onChange={(e) => { cert.key = e.target.value; refresh(); }} />
- </Form.Item>
- </>
- )}
- <Form.Item label="One Time Loading"><Switch checked={!!cert.oneTimeLoading} onChange={(v) => { cert.oneTimeLoading = v; refresh(); }} /></Form.Item>
- <Form.Item label="Usage Option">
- <Select value={cert.usage} style={{ width: '50%' }} onChange={(v) => { cert.usage = v; refresh(); }}>
- {USAGES.map((u) => <Select.Option key={u} value={u}>{u}</Select.Option>)}
- </Select>
- </Form.Item>
- {cert.usage === 'issue' && (
- <Form.Item label="Build Chain"><Switch checked={!!cert.buildChain} onChange={(v) => { cert.buildChain = v; refresh(); }} /></Form.Item>
- )}
- </div>
- ))}
- <Form.Item label="ECH key"><Input value={ib.stream.tls.echServerKeys} onChange={(e) => { ib.stream.tls.echServerKeys = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="ECH config"><Input value={ib.stream.tls.settings.echConfigList} onChange={(e) => { ib.stream.tls.settings.echConfigList = e.target.value; refresh(); }} /></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>
- </>
- )}
- {ib.stream.security === 'reality' && ib.stream.reality && (
- <>
- <Form.Item label="Show"><Switch checked={!!ib.stream.reality.show} onChange={(v) => { ib.stream.reality.show = v; refresh(); }} /></Form.Item>
- <Form.Item label="Xver"><InputNumber value={ib.stream.reality.xver} min={0} onChange={(v) => { ib.stream.reality.xver = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="uTLS">
- <Select value={ib.stream.reality.settings.fingerprint} style={{ width: '100%' }} onChange={(v) => { ib.stream.reality.settings.fingerprint = v; refresh(); }}>
- {FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
- </Select>
- </Form.Item>
- <Form.Item label={<>Target <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /></>}>
- <Input value={ib.stream.reality.target} onChange={(e) => { ib.stream.reality.target = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={<>SNI <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /></>}>
- <Input value={ib.stream.reality.serverNames} onChange={(e) => { ib.stream.reality.serverNames = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="Max Time Diff (ms)"><InputNumber value={ib.stream.reality.maxTimediff} min={0} onChange={(v) => { ib.stream.reality.maxTimediff = Number(v) || 0; refresh(); }} /></Form.Item>
- <Form.Item label="Min Client Ver"><Input value={ib.stream.reality.minClientVer} placeholder="25.9.11" onChange={(e) => { ib.stream.reality.minClientVer = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label="Max Client Ver"><Input value={ib.stream.reality.maxClientVer} placeholder="25.9.11" onChange={(e) => { ib.stream.reality.maxClientVer = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label={<>Short IDs <SyncOutlined className="random-icon" onClick={randomizeShortIds} /></>}>
- <TextArea value={ib.stream.reality.shortIds} autoSize={{ minRows: 1, maxRows: 4 }} onChange={(e) => { ib.stream.reality.shortIds = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="SpiderX"><Input value={ib.stream.reality.settings.spiderX} onChange={(e) => { ib.stream.reality.settings.spiderX = e.target.value; refresh(); }} /></Form.Item>
- <Form.Item label={t('pages.inbounds.publicKey')}>
- <TextArea value={ib.stream.reality.settings.publicKey} autoSize={{ minRows: 1, maxRows: 4 }}
- onChange={(e) => { ib.stream.reality.settings.publicKey = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.privatekey')}>
- <TextArea value={ib.stream.reality.privateKey} autoSize={{ minRows: 1, maxRows: 4 }}
- onChange={(e) => { ib.stream.reality.privateKey = e.target.value; refresh(); }} />
- </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 label="mldsa65 Seed">
- <TextArea value={ib.stream.reality.mldsa65Seed} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.reality.mldsa65Seed = e.target.value; refresh(); }} />
- </Form.Item>
- <Form.Item label="mldsa65 Verify">
- <TextArea value={ib.stream.reality.settings.mldsa65Verify} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.reality.settings.mldsa65Verify = e.target.value; refresh(); }} />
- </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>
- </>
- )}
- </Form>
- );
- const renderSniffingTab = () => (
- <Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
- <Form.Item label={t('enable')}>
- <Switch checked={!!ib.sniffing.enabled} onChange={(v) => { ib.sniffing.enabled = v; refresh(); }} />
- </Form.Item>
- {ib.sniffing.enabled && (
- <>
- <Form.Item wrapperCol={{ span: 24 }}>
- <Checkbox.Group value={ib.sniffing.destOverride} onChange={(v) => { ib.sniffing.destOverride = v; refresh(); }}>
- {Object.entries(SNIFFING_OPTION).map(([key, value]) => (
- <Checkbox key={key} value={value}>{key}</Checkbox>
- ))}
- </Checkbox.Group>
- </Form.Item>
- <Form.Item label={t('pages.inbounds.sniffingMetadataOnly')}>
- <Switch checked={!!ib.sniffing.metadataOnly} onChange={(v) => { ib.sniffing.metadataOnly = v; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.sniffingRouteOnly')}>
- <Switch checked={!!ib.sniffing.routeOnly} onChange={(v) => { ib.sniffing.routeOnly = v; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.sniffingIpsExcluded')}>
- <Select mode="tags" value={ib.sniffing.ipsExcluded} tokenSeparators={[',']}
- placeholder="IP/CIDR/geoip:*/ext:*" style={{ width: '100%' }}
- onChange={(v) => { ib.sniffing.ipsExcluded = v; refresh(); }} />
- </Form.Item>
- <Form.Item label={t('pages.inbounds.sniffingDomainsExcluded')}>
- <Select mode="tags" value={ib.sniffing.domainsExcluded} tokenSeparators={[',']}
- placeholder="domain:*/ext:*" style={{ width: '100%' }}
- onChange={(v) => { ib.sniffing.domainsExcluded = v; refresh(); }} />
- </Form.Item>
- </>
- )}
- </Form>
- );
- const renderAdvancedTab = () => {
- const advancedTabItems = [
- {
- key: 'all',
- label: t('pages.inbounds.advanced.all'),
- children: (
- <>
- <div className="advanced-editor-meta">{t('pages.inbounds.advanced.allHelp')}</div>
- <JsonEditor value={advancedAllValue} onChange={setAdvancedAllValue} minHeight="340px" maxHeight="560px" />
- </>
- ),
- },
- {
- key: 'settings',
- label: t('pages.inbounds.advanced.settings'),
- children: (
- <>
- <div className="advanced-editor-meta">
- {t('pages.inbounds.advanced.settingsHelp')} <code>{'{ settings: { ... } }'}</code>.
- </div>
- <JsonEditor value={wrappedConfigValue('settings', 'settings')}
- onChange={(v) => setWrappedConfigValue('settings', 'settings', 'Settings', v)}
- minHeight="320px" maxHeight="540px" />
- </>
- ),
- },
- {
- key: 'sniffingSection',
- label: t('pages.inbounds.advanced.sniffing'),
- children: (
- <>
- <div className="advanced-editor-meta">
- {t('pages.inbounds.advanced.sniffingHelp')} <code>{'{ sniffing: { ... } }'}</code>.
- </div>
- <JsonEditor value={wrappedConfigValue('sniffing', 'sniffing')}
- onChange={(v) => setWrappedConfigValue('sniffing', 'sniffing', 'Sniffing', v)}
- minHeight="240px" maxHeight="420px" />
- </>
- ),
- },
- ];
- if (canEnableStream) {
- advancedTabItems.push({
- key: 'streamSection',
- label: t('pages.inbounds.advanced.stream'),
- children: (
- <>
- <div className="advanced-editor-meta">
- {t('pages.inbounds.advanced.streamHelp')} <code>{'{ streamSettings: { ... } }'}</code>.
- </div>
- <JsonEditor value={wrappedConfigValue('streamSettings', 'stream')}
- onChange={(v) => setWrappedConfigValue('streamSettings', 'stream', 'Stream', v)}
- minHeight="320px" maxHeight="540px" />
- </>
- ),
- });
- }
- return (
- <div className="advanced-shell">
- <div className="advanced-panel">
- <div className="advanced-panel__header">
- <div>
- <div className="advanced-panel__title">{t('pages.inbounds.advanced.title')}</div>
- <div className="advanced-panel__subtitle">{t('pages.inbounds.advanced.subtitle')}</div>
- </div>
- </div>
- <Tabs activeKey={advancedSectionKey} onChange={setAdvancedSectionKey} items={advancedTabItems} className="advanced-inner-tabs" />
- </div>
- </div>
- );
- };
- const tabItems = [
- { key: 'basic', label: t('pages.xray.basicTemplate'), children: renderBasicsTab() },
- ];
- if (hasProtocolTabContent) {
- tabItems.push({ key: 'protocol', label: t('pages.inbounds.protocol'), children: renderProtocolTab() });
- }
- if (canEnableStream) {
- tabItems.push({ key: 'stream', label: t('pages.inbounds.streamTab'), children: renderStreamTab() });
- tabItems.push({ key: 'security', label: t('pages.inbounds.securityTab'), children: renderSecurityTab() });
- }
- tabItems.push({ key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: renderSniffingTab() });
- tabItems.push({ key: 'advanced', label: t('pages.xray.advancedTemplate'), children: renderAdvancedTab() });
- return (
- <>
- {messageContextHolder}
- <Modal
- open={open}
- title={title}
- okText={okText}
- cancelText={t('close')}
- confirmLoading={saving}
- mask={{ closable: false }}
- width={780}
- onOk={submit}
- onCancel={onClose}
- destroyOnHidden
- >
- <Tabs activeKey={activeTabKey} onChange={handleTabChange} items={tabItems} />
- </Modal>
- </>
- );
- }
|