mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-13 01:14:59 +03:00
fix(finalmask): refuse a salamander password xray-core cannot open
xray-core needs a salamander (and gecko) password of at least 4 bytes (smPSKMinLen), but checks it only when a socket opens: xray run -test reports Configuration OK. On an inbound the listener then fails and Xray does not start at all, taking every other inbound down; on a client the dial fails. The panel saved any password, and the finalmask editor starts a new salamander mask with an empty one. maskcompat.CheckSalamanderPasswords applies the core's byte rule, pinned against the core's own ListenPacket, and every path that stores a finalmask refuses a short password: inbound add and update, hosts, the subscription's global finalmask, and the inbounds and outbounds of the Xray template. The editor flags it inline as it is typed.
This commit is contained in:
@@ -88,6 +88,20 @@ function validateGeckoPacketSize(_rule: unknown, value: unknown): Promise<void>
|
||||
);
|
||||
}
|
||||
|
||||
// xray-core's salamander smPSKMinLen counts bytes; a shorter password loads, then the listener
|
||||
// (stopping Xray) or the dial fails, so the backend refuses it at save too.
|
||||
const SALAMANDER_MIN_PSK_BYTES = 4;
|
||||
|
||||
function validateSalamanderPassword(_rule: unknown, value: unknown): Promise<void> {
|
||||
const bytes = new TextEncoder().encode(typeof value === 'string' ? value : '').length;
|
||||
if (bytes >= SALAMANDER_MIN_PSK_BYTES) return Promise.resolve();
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`Use at least ${SALAMANDER_MIN_PSK_BYTES} bytes; xray-core refuses a shorter password`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function asPath(name: NamePath): (string | number)[] {
|
||||
return Array.isArray(name) ? [...name] : [name];
|
||||
}
|
||||
@@ -1198,7 +1212,11 @@ function SalamanderUdpMaskSettings({
|
||||
|
||||
<Form.Item label="Password">
|
||||
<Space.Compact block>
|
||||
<Form.Item name={[fieldName, 'settings', 'password']} noStyle>
|
||||
<Form.Item
|
||||
name={[fieldName, 'settings', 'password']}
|
||||
noStyle
|
||||
rules={[{ validator: validateSalamanderPassword }]}
|
||||
>
|
||||
<Input placeholder="Obfuscation password" style={{ width: 'calc(100% - 32px)' }} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { FinalMaskField } from '@/lib/xray/forms/fields';
|
||||
|
||||
@@ -27,3 +28,24 @@ describe('FinalMaskForm item types', () => {
|
||||
expect(listSelectOptions('finalmask_udp_0_settings_noise_0_type')).toContain('Expression');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FinalMaskForm salamander password', () => {
|
||||
// xray-core loads a salamander password under 4 bytes and refuses it only when the socket
|
||||
// opens: on an inbound that stops Xray. It counts bytes, so "a€" (4 bytes) is long enough.
|
||||
it('flags a password under 4 bytes and accepts a 4-byte one', async () => {
|
||||
renderWithProviders(
|
||||
<FinalMaskField
|
||||
network="hysteria"
|
||||
protocol="hysteria"
|
||||
value={{ tcp: [], udp: [{ type: 'salamander', settings: { password: 'long-enough' } }] }}
|
||||
/>,
|
||||
);
|
||||
const input = document.getElementById('finalmask_udp_0_settings_password') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: 'abc' } });
|
||||
expect(await screen.findByText(/at least 4 bytes/)).toBeTruthy();
|
||||
|
||||
fireEvent.change(input, { target: { value: 'a€' } });
|
||||
await waitFor(() => expect(screen.queryByText(/at least 4 bytes/)).toBeNull());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user