mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 23:35:00 +03:00
feat(tls): cipher suite presets and a uTLS fingerprint warning
Adds PattN's semi-python and real-firefox client cipher lists as presets in the cipher suites picker. A preset replaces the whole list because the order is part of the ClientHello. The inbound TLS form now warns when cipher suites are set under a uTLS fingerprint other than "unsafe": clients ignore them there, while an empty fingerprint is already pinned to unsafe in share links.
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { Select } from 'antd';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TLS_CIPHER_OPTION } from '@/schemas/primitives';
|
||||
import { TLS_CIPHER_OPTION, TLS_CIPHER_PRESETS } from '@/schemas/primitives';
|
||||
|
||||
const CIPHER_SUITE_OPTIONS = Object.values(TLS_CIPHER_OPTION).map((v) => ({ value: v, label: v }));
|
||||
|
||||
const PRESET_PREFIX = 'preset:';
|
||||
const PRESET_OPTIONS = Object.keys(TLS_CIPHER_PRESETS).map((name) => ({
|
||||
value: PRESET_PREFIX + name,
|
||||
label: name,
|
||||
}));
|
||||
|
||||
type CipherSuitesSelectProps = Omit<
|
||||
SelectProps<string[]>,
|
||||
'value' | 'onChange' | 'mode' | 'options'
|
||||
@@ -14,6 +21,13 @@ type CipherSuitesSelectProps = Omit<
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
function expandPreset(next: string[]): string[] {
|
||||
const picked = [...next].reverse().find((v) => v.startsWith(PRESET_PREFIX));
|
||||
if (!picked) return next;
|
||||
const name = picked.slice(PRESET_PREFIX.length) as keyof typeof TLS_CIPHER_PRESETS;
|
||||
return TLS_CIPHER_PRESETS[name].split(':');
|
||||
}
|
||||
|
||||
// xray splits cipherSuites on ':' into a list, so the picker edits tags while
|
||||
// the stored value stays the single colon-joined string xray reads.
|
||||
export default function CipherSuitesSelect({
|
||||
@@ -21,6 +35,7 @@ export default function CipherSuitesSelect({
|
||||
onChange,
|
||||
...rest
|
||||
}: CipherSuitesSelectProps) {
|
||||
const { t } = useTranslation();
|
||||
const suites = value
|
||||
.split(':')
|
||||
.map((s) => s.trim())
|
||||
@@ -31,9 +46,12 @@ export default function CipherSuitesSelect({
|
||||
tokenSeparators={[':', ',']}
|
||||
{...rest}
|
||||
mode="tags"
|
||||
options={CIPHER_SUITE_OPTIONS}
|
||||
options={[
|
||||
{ label: t('pages.inbounds.form.presets'), options: PRESET_OPTIONS },
|
||||
{ label: t('pages.inbounds.form.cipherSuites'), options: CIPHER_SUITE_OPTIONS },
|
||||
]}
|
||||
value={suites}
|
||||
onChange={(next) => onChange?.(next.join(':'))}
|
||||
onChange={(next) => onChange?.(expandPreset(next).join(':'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -325,6 +325,14 @@ export default function TlsForm({
|
||||
control,
|
||||
name: 'streamSettings.tlsSettings.certificates',
|
||||
});
|
||||
const cipherSuites = useWatch({ control, name: 'streamSettings.tlsSettings.cipherSuites' });
|
||||
const fingerprint = useWatch({
|
||||
control,
|
||||
name: 'streamSettings.tlsSettings.settings.fingerprint',
|
||||
});
|
||||
// An empty fingerprint is fine: share links pin it to unsafe whenever cs is set.
|
||||
const cipherSuitesIgnoredByClients =
|
||||
Boolean(cipherSuites) && Boolean(fingerprint) && fingerprint !== UTLS_FINGERPRINT.UTLS_UNSAFE;
|
||||
return (
|
||||
<>
|
||||
<FormField name={['streamSettings', 'tlsSettings', 'serverName']} label="SNI">
|
||||
@@ -333,6 +341,13 @@ export default function TlsForm({
|
||||
<FormField
|
||||
name={['streamSettings', 'tlsSettings', 'cipherSuites']}
|
||||
label={t('pages.inbounds.form.cipherSuites')}
|
||||
extra={
|
||||
cipherSuitesIgnoredByClients ? (
|
||||
<Typography.Text type="warning" data-testid="cipher-suites-fingerprint-warning">
|
||||
{t('pages.inbounds.form.cipherSuitesNeedUnsafe')}
|
||||
</Typography.Text>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<CipherSuitesSelect placeholder={t('pages.inbounds.form.autoOption')} />
|
||||
</FormField>
|
||||
|
||||
@@ -74,6 +74,37 @@ export const TLS_CIPHER_OPTION = Object.freeze({
|
||||
ECDHE_RSA_CHACHA20_POLY1305: 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
|
||||
});
|
||||
|
||||
// Whole ordered client lists (PattN's presets): the order is part of the ClientHello,
|
||||
// and clients apply them only under the uTLS fingerprint "unsafe".
|
||||
export const TLS_CIPHER_PRESETS = Object.freeze({
|
||||
'semi-python': [
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256',
|
||||
].join(':'),
|
||||
'real-firefox': [
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_RSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_RSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_RSA_WITH_AES_256_CBC_SHA',
|
||||
].join(':'),
|
||||
});
|
||||
|
||||
export const USAGE_OPTION = Object.freeze({
|
||||
ENCIPHERMENT: 'encipherment',
|
||||
VERIFY: 'verify',
|
||||
|
||||
@@ -24,6 +24,22 @@ describe('CipherSuitesSelect', () => {
|
||||
expect(onChange).toHaveBeenLastCalledWith('TLS_AES_256_GCM_SHA384:MY_CUSTOM_SUITE');
|
||||
});
|
||||
|
||||
// A preset is a whole ordered list (the order is part of the ClientHello), so
|
||||
// picking one replaces the suites instead of appending a tag named after it.
|
||||
it('replaces the suites with the picked preset list', async () => {
|
||||
const onChange = renderSelect('TLS_AES_256_GCM_SHA384');
|
||||
fireEvent.mouseDown(screen.getByRole('combobox', { name: 'cipher suites' }));
|
||||
fireEvent.click(await screen.findByText('real-firefox'));
|
||||
expect(onChange).toHaveBeenLastCalledWith(
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:' +
|
||||
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:' +
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:' +
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:' +
|
||||
'TLS_RSA_WITH_AES_128_GCM_SHA256:TLS_RSA_WITH_AES_256_GCM_SHA384:' +
|
||||
'TLS_RSA_WITH_AES_128_CBC_SHA:TLS_RSA_WITH_AES_256_CBC_SHA',
|
||||
);
|
||||
});
|
||||
|
||||
it('stores an empty string once every suite is removed', () => {
|
||||
const onChange = renderSelect('TLS_AES_256_GCM_SHA384');
|
||||
const remove = document.querySelector('.ant-select-selection-item-remove');
|
||||
|
||||
@@ -136,3 +136,26 @@ describe('inbound TLS dynamic ECH helper', () => {
|
||||
expect(screen.getByTestId('ech-cdn-warning').textContent).toMatch(/Cloudflare CDN/i);
|
||||
});
|
||||
});
|
||||
|
||||
// Xray drops cipherSuites under every uTLS fingerprint but unsafe; an empty one
|
||||
// is left out because share links pin it to unsafe.
|
||||
describe('inbound TLS cipherSuites fingerprint warning', () => {
|
||||
function tlsValues(cipherSuites: string, fingerprint: string) {
|
||||
return { streamSettings: { tlsSettings: { cipherSuites, settings: { fingerprint } } } };
|
||||
}
|
||||
|
||||
it('warns when cipher suites sit under a uTLS fingerprint', async () => {
|
||||
renderTls(tlsValues('TLS_AES_128_GCM_SHA256', 'chrome'));
|
||||
expect(await screen.findByTestId('cipher-suites-fingerprint-warning')).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['unsafe fingerprint', 'TLS_AES_128_GCM_SHA256', 'unsafe'],
|
||||
['empty fingerprint', 'TLS_AES_128_GCM_SHA256', ''],
|
||||
['no cipher suites', '', 'chrome'],
|
||||
])('stays silent with %s', async (_, cipherSuites, fingerprint) => {
|
||||
renderTls(tlsValues(cipherSuites, fingerprint));
|
||||
await screen.findByTestId('ech-dynamic-helper');
|
||||
expect(screen.queryByTestId('cipher-suites-fingerprint-warning')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
"addCustomOption": "إضافة خيار مخصص",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "مجموعات التشفير",
|
||||
"cipherSuitesNeedUnsafe": "يطبّق العملاء مجموعات التشفير هذه فقط مع بصمة uTLS \"unsafe\"، وتُتجاهل مع أي بصمة أخرى.",
|
||||
"presets": "إعدادات مسبقة",
|
||||
"autoOption": "تلقائي",
|
||||
"minMaxVersion": "إصدار أدنى/أقصى",
|
||||
"rejectUnknownSni": "رفض SNI غير معروف",
|
||||
|
||||
@@ -603,6 +603,8 @@
|
||||
"addCustomOption": "Add custom option",
|
||||
"serverNameIndication": "Server Name Indication",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuitesNeedUnsafe": "Clients apply these cipher suites only with the uTLS fingerprint \"unsafe\"; under any other fingerprint they are ignored.",
|
||||
"presets": "Presets",
|
||||
"autoOption": "Auto",
|
||||
"minMaxVersion": "Min/Max Version",
|
||||
"rejectUnknownSni": "Reject Unknown SNI",
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
"addCustomOption": "Añadir opción personalizada",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Conjuntos de cifrado",
|
||||
"cipherSuitesNeedUnsafe": "Los clientes solo aplican estos cipher suites con la huella uTLS \"unsafe\"; con cualquier otra huella se ignoran.",
|
||||
"presets": "Preajustes",
|
||||
"autoOption": "Auto",
|
||||
"minMaxVersion": "Versión mín/máx",
|
||||
"rejectUnknownSni": "Rechazar SNI desconocido",
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
"addCustomOption": "افزودن گزینه دلخواه",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "مجموعههای رمز",
|
||||
"cipherSuitesNeedUnsafe": "کلاینتها این cipher suiteها را فقط با فینگرپرینت uTLS «unsafe» اعمال میکنند و با هر فینگرپرینت دیگری نادیده گرفته میشوند.",
|
||||
"presets": "پیشتنظیمها",
|
||||
"autoOption": "خودکار",
|
||||
"minMaxVersion": "نسخه حداقل/حداکثر",
|
||||
"rejectUnknownSni": "رد SNI ناشناخته",
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
"addCustomOption": "Tambah opsi kustom",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Rangkaian Sandi",
|
||||
"cipherSuitesNeedUnsafe": "Klien hanya menerapkan cipher suite ini dengan fingerprint uTLS \"unsafe\"; dengan fingerprint lain akan diabaikan.",
|
||||
"presets": "Preset",
|
||||
"autoOption": "Otomatis",
|
||||
"minMaxVersion": "Versi Min/Maks",
|
||||
"rejectUnknownSni": "Tolak SNI tidak dikenal",
|
||||
|
||||
@@ -652,6 +652,8 @@
|
||||
"addCustomOption": "カスタムオプション追加",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "暗号スイート",
|
||||
"cipherSuitesNeedUnsafe": "クライアントはこれらの暗号スイートを uTLS フィンガープリント \"unsafe\" の場合にのみ適用し、それ以外のフィンガープリントでは無視します。",
|
||||
"presets": "プリセット",
|
||||
"autoOption": "自動",
|
||||
"minMaxVersion": "最小/最大バージョン",
|
||||
"rejectUnknownSni": "未知の SNI を拒否",
|
||||
|
||||
@@ -652,6 +652,8 @@
|
||||
"addCustomOption": "Adicionar opção personalizada",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Conjuntos de cifras",
|
||||
"cipherSuitesNeedUnsafe": "Os clientes só aplicam estes cipher suites com a impressão digital uTLS \"unsafe\"; com qualquer outra eles são ignorados.",
|
||||
"presets": "Predefinições",
|
||||
"autoOption": "Auto",
|
||||
"minMaxVersion": "Versão mín/máx",
|
||||
"rejectUnknownSni": "Rejeitar SNI desconhecido",
|
||||
|
||||
@@ -654,6 +654,8 @@
|
||||
"addCustomOption": "Добавить опцию",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Наборы шифров",
|
||||
"cipherSuitesNeedUnsafe": "Клиенты применяют эти наборы шифров только с отпечатком uTLS «unsafe»; при любом другом отпечатке они игнорируются.",
|
||||
"presets": "Пресеты",
|
||||
"autoOption": "Авто",
|
||||
"minMaxVersion": "Мин/Макс версия",
|
||||
"rejectUnknownSni": "Отклонить неизвестный SNI",
|
||||
|
||||
@@ -603,6 +603,8 @@
|
||||
"addCustomOption": "Pridať vlastné nastavenie",
|
||||
"serverNameIndication": "Názov servera (SNI)",
|
||||
"cipherSuites": "Šifrovacie sady",
|
||||
"cipherSuitesNeedUnsafe": "Klienti použijú tieto šifrovacie sady iba s odtlačkom uTLS „unsafe“; pri akomkoľvek inom odtlačku sa ignorujú.",
|
||||
"presets": "Predvoľby",
|
||||
"autoOption": "Automaticky",
|
||||
"minMaxVersion": "Najnižšia/najvyššia verzia",
|
||||
"rejectUnknownSni": "Odmietnuť neznáme SNI",
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
"addCustomOption": "Özel Seçenek Ekle",
|
||||
"serverNameIndication": "Server Name Indication (SNI)",
|
||||
"cipherSuites": "Şifreleme Takımları",
|
||||
"cipherSuitesNeedUnsafe": "İstemciler bu şifre paketlerini yalnızca \"unsafe\" uTLS parmak iziyle uygular; başka bir parmak izinde yok sayılır.",
|
||||
"presets": "Hazır ayarlar",
|
||||
"autoOption": "Otomatik",
|
||||
"minMaxVersion": "Min/Maks Sürüm",
|
||||
"rejectUnknownSni": "Bilinmeyen SNI Reddet",
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
"addCustomOption": "Додати опцію",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Набори шифрів",
|
||||
"cipherSuitesNeedUnsafe": "Клієнти застосовують ці набори шифрів лише з відбитком uTLS «unsafe»; з будь-яким іншим відбитком вони ігноруються.",
|
||||
"presets": "Пресети",
|
||||
"autoOption": "Авто",
|
||||
"minMaxVersion": "Мін/Макс версія",
|
||||
"rejectUnknownSni": "Відхиляти невідомий SNI",
|
||||
|
||||
@@ -652,6 +652,8 @@
|
||||
"addCustomOption": "Thêm tùy chọn",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Bộ mật mã",
|
||||
"cipherSuitesNeedUnsafe": "Client chỉ áp dụng các cipher suite này với fingerprint uTLS \"unsafe\"; với fingerprint khác chúng sẽ bị bỏ qua.",
|
||||
"presets": "Cấu hình sẵn",
|
||||
"autoOption": "Tự động",
|
||||
"minMaxVersion": "Phiên bản Min/Max",
|
||||
"rejectUnknownSni": "Từ chối SNI lạ",
|
||||
|
||||
@@ -651,6 +651,8 @@
|
||||
"addCustomOption": "添加自定义选项",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "密码套件",
|
||||
"cipherSuitesNeedUnsafe": "客户端仅在 uTLS 指纹为 \"unsafe\" 时应用这些加密套件;使用其他指纹时会被忽略。",
|
||||
"presets": "预设",
|
||||
"autoOption": "自动",
|
||||
"minMaxVersion": "最小/最大版本",
|
||||
"rejectUnknownSni": "拒绝未知 SNI",
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
"addCustomOption": "新增自訂選項",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "加密套件",
|
||||
"cipherSuitesNeedUnsafe": "用戶端僅在 uTLS 指紋為 \"unsafe\" 時套用這些加密套件;使用其他指紋時會被忽略。",
|
||||
"presets": "預設集",
|
||||
"autoOption": "自動",
|
||||
"minMaxVersion": "最小/最大版本",
|
||||
"rejectUnknownSni": "拒絕未知 SNI",
|
||||
|
||||
Reference in New Issue
Block a user