mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-23 22:35:03 +03:00
feat(xray): update xray-core to v26.10.21 and adapt panel
Bump xtls/xray-core to 1b1647cd9ddf (v26.10.21) and move the three binary pins (DockerInit.sh, both release.yml fetches) in lockstep. The v26.10.10 release was pulled upstream, so the old pins already 404. XDRIVE (#7182): the core now runs XDRIVE only for VLESS inside VLESS encryption, with no flow, and an outbound only over Mux; it refuses the whole config otherwise. The panel offers XDRIVE only for VLESS, refuses to save an inbound without decryption, never emits a client flow on an XDRIVE stream (config, JSON subscription, forms), and turns Mux on when an outbound picks XDRIVE. An inbound or template outbound stored before the upgrade that the core would refuse is left out of the generated config with a warning, so it no longer stops every other inbound. The service "Google Drive" is now "google-drive" (old name read as a deprecated alias); saves, the schema and both form adapters store the new name. XDRIVE and MASQUE gain a client xmux; both schemas keep it instead of stripping it on an inbound save. Lua scripts (#7124, #7136): the core refuses a routing script beside rules or domainStrategy, and a dns script beside the fallback/parallel switches or a domain-scoped server. The panel always keeps its api routing rule, so a template save now refuses any routing script and a dns script in a refused combination, and a stored one is dropped from the generated config so the core still starts. Noise masks gain a Lua script that the loader compiles inside Build: the in-process build (template save check, hot add) now resolves a relative script the way the xray child does, beside the xray binary, instead of failing it. The mask editor gets a Script field that replaces the noise list. XDNS (#7140): a resolver takes https:// DoH addresses with worker, tlsConfig and quicConfig. The mask editor registered only addrs, so a save dropped the rest; it now edits all four. Finalmask filter (#7206): a new TCP/UDP mask screens an inbound's source IPs. The inbound mask editor offers it with its allow and block lists; it is a no-op on a dialer and an older client core refuses the type, so the share link and JSON subscription drop it like the Go link already does.
This commit is contained in:
@@ -54,7 +54,7 @@ exports[`NetworkSettingsSchema fixtures > parses xdrive-google-drive byte-stably
|
||||
"1//example-refresh-token",
|
||||
],
|
||||
"segmentBytes": 524288,
|
||||
"service": "Google Drive",
|
||||
"service": "google-drive",
|
||||
"sessionTtlSeconds": 300,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -29,6 +29,24 @@ describe('FinalMaskForm item types', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('FinalMaskForm filter mask', () => {
|
||||
// xray-core 26.10.21's filter screens a listener's source IPs and is a no-op on a dialer, so
|
||||
// only the inbound editor offers it; an older client core refuses the unknown type.
|
||||
it('offers filter on the server side only', () => {
|
||||
const value = { tcp: [{ type: 'fragment' as const, settings: {} }], udp: [] };
|
||||
const client = renderWithProviders(
|
||||
<FinalMaskField network="tcp" protocol="vless" side="client" value={value} />,
|
||||
);
|
||||
expect(listSelectOptions('finalmask_tcp_0_type')).not.toContain('Filter (source IPs)');
|
||||
client.unmount();
|
||||
|
||||
renderWithProviders(
|
||||
<FinalMaskField network="tcp" protocol="vless" side="server" value={value} />,
|
||||
);
|
||||
expect(listSelectOptions('finalmask_tcp_0_type')).toContain('Filter (source IPs)');
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"address": "142.250.0.1",
|
||||
"port": 443,
|
||||
"xdriveSettings": {
|
||||
"service": "Google Drive",
|
||||
"service": "google-drive",
|
||||
"remoteFolder": "xray-tunnel",
|
||||
"secrets": [
|
||||
"000000000000-example.apps.googleusercontent.com",
|
||||
|
||||
@@ -371,7 +371,7 @@ describe('InboundFormModal', () => {
|
||||
network: 'xdrive',
|
||||
security: 'none',
|
||||
xdriveSettings: {
|
||||
service: 'Google Drive',
|
||||
service: 'google-drive',
|
||||
remoteFolder: 'xray-tunnel',
|
||||
secrets: ['id.apps.googleusercontent.com', 'client-secret', 'refresh-token'],
|
||||
},
|
||||
@@ -467,6 +467,54 @@ describe('InboundFormModal', () => {
|
||||
expect(JSON.stringify(finalmask.udp ?? [])).not.toContain('udphop');
|
||||
});
|
||||
|
||||
// xray-core 26.10.21 (#7206) screens a listener's source IPs with the "filter" mask.
|
||||
it('saves an inbound filter mask with both source lists', async () => {
|
||||
const post = vi.mocked(HttpUtil.post);
|
||||
post.mockClear();
|
||||
const filter = {
|
||||
type: 'filter',
|
||||
settings: { allowedSourceIPs: ['10.0.0.0/8'], blockedSourceIPs: ['geoip:cn'] },
|
||||
};
|
||||
const hysteria = new DBInbound({
|
||||
id: 45,
|
||||
port: 443,
|
||||
listen: '',
|
||||
protocol: 'hysteria',
|
||||
remark: 'hy',
|
||||
enable: true,
|
||||
settings: { version: 2, clients: [] },
|
||||
streamSettings: {
|
||||
network: 'hysteria',
|
||||
security: 'tls',
|
||||
hysteriaSettings: { version: 2, auth: '', udpIdleTimeout: 60 },
|
||||
tlsSettings: {
|
||||
serverName: 'hy.example.com',
|
||||
alpn: ['h3'],
|
||||
certificates: [
|
||||
{ certificateFile: '/etc/ssl/certs/hy.crt', keyFile: '/etc/ssl/private/hy.key' },
|
||||
],
|
||||
},
|
||||
finalmask: { udp: [filter] },
|
||||
},
|
||||
sniffing: { enabled: false },
|
||||
nodeId: null,
|
||||
shareAddrStrategy: 'listen',
|
||||
shareAddr: '',
|
||||
});
|
||||
renderCloneLikeEdit(hysteria);
|
||||
await screen.findByText('geoip:cn');
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
fireEvent.click(primaryButton());
|
||||
|
||||
const isUpdate = ([url]: unknown[]) => url === '/panel/api/inbounds/update/45';
|
||||
await waitFor(() => expect(post.mock.calls.some(isUpdate)).toBe(true));
|
||||
const payload = post.mock.calls.find(isUpdate)![1] as { streamSettings: string };
|
||||
expect(JSON.parse(payload.streamSettings).finalmask.udp).toEqual([filter]);
|
||||
});
|
||||
|
||||
// xray-core 26.9: the Wintun adapter type (desc) and the TUN auto-system leak guards.
|
||||
it('saves the TUN adapter type and auto-system options set in the form', async () => {
|
||||
const post = vi.mocked(HttpUtil.post);
|
||||
|
||||
@@ -226,8 +226,9 @@ describe('protocol-capability helpers with raw coerced shapes', () => {
|
||||
streamSettings: { network: 'xhttp', security: 'none' },
|
||||
}),
|
||||
).toBe(false);
|
||||
// xray-core runs Vision inside VLESS encryption on any transport, RAW and WS included.
|
||||
for (const network of ['tcp', 'ws', 'grpc', 'xdrive']) {
|
||||
// xray-core runs Vision inside VLESS encryption on any transport, RAW and WS included,
|
||||
// but XDRIVE, which refuses a flow since 26.10.17 (#7182).
|
||||
for (const network of ['tcp', 'ws', 'grpc']) {
|
||||
expect(
|
||||
canEnableTlsFlow({
|
||||
protocol: 'vless',
|
||||
@@ -236,6 +237,13 @@ describe('protocol-capability helpers with raw coerced shapes', () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
}
|
||||
expect(
|
||||
canEnableTlsFlow({
|
||||
protocol: 'vless',
|
||||
settings: { decryption: dec, encryption: enc },
|
||||
streamSettings: { network: 'xdrive', security: 'none' },
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canEnableTlsFlow({
|
||||
protocol: 'vless',
|
||||
|
||||
@@ -288,6 +288,22 @@ describe('genVlessLink TCP fragment finalmask compatibility', () => {
|
||||
expect(exported).toEqual(finalmask);
|
||||
});
|
||||
|
||||
// xray-core 26.10.21's filter screens the listener's own source IPs; a client core older than
|
||||
// it refuses the unknown type, and the lists would hand clients the server's IP policy.
|
||||
it('leaves the inbound-only filter mask out of the link', () => {
|
||||
const exported = finalmaskFrom(
|
||||
linkFor({
|
||||
tcp: [{ type: 'filter', settings: { allowedSourceIPs: ['10.0.0.0/8'] } }],
|
||||
udp: [
|
||||
{ type: 'filter', settings: { blockedSourceIPs: ['geoip:cn'] } },
|
||||
{ type: 'salamander', settings: { password: 'p' } },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(exported).toEqual({ udp: [{ type: 'salamander', settings: { password: 'p' } }] });
|
||||
});
|
||||
|
||||
it('exports a stored UDP-only finalmask whose empty tcp list was dropped on save', () => {
|
||||
const udpOnly = { udp: [{ type: 'salamander', settings: { password: 'p' } }] };
|
||||
const inbound = inboundFromDb({
|
||||
|
||||
@@ -354,7 +354,7 @@ describe('OutboundFormModal', () => {
|
||||
expect(formItemByLabel('Mux')).toBeDefined();
|
||||
ws.unmount();
|
||||
|
||||
await openModal(
|
||||
const xhttp = await openModal(
|
||||
vless('mlkem768x25519plus.native.0rtt.key', {
|
||||
network: 'xhttp',
|
||||
security: 'none',
|
||||
@@ -362,6 +362,68 @@ describe('OutboundFormModal', () => {
|
||||
}),
|
||||
);
|
||||
expect(formItemByLabel('Flow')).toBeDefined();
|
||||
xhttp.unmount();
|
||||
|
||||
// xray-core 26.10.17 (#7182) refuses a flow on XDRIVE even inside VLESS encryption.
|
||||
await openModal(
|
||||
vless('mlkem768x25519plus.native.0rtt.key', {
|
||||
network: 'xdrive',
|
||||
security: 'none',
|
||||
xdriveSettings: { service: 'local', remoteFolder: '/srv/drive' },
|
||||
}),
|
||||
);
|
||||
expect(formItemByLabel('Transmission')?.textContent).toContain('XDRIVE');
|
||||
expect(formItemByLabel('Flow')).toBeUndefined();
|
||||
});
|
||||
|
||||
// xray-core 26.10.17 (#7182) carries only VLESS over XDRIVE.
|
||||
it('offers XDRIVE only to a VLESS outbound', async () => {
|
||||
const transmissionOptions = async (protocol: string) => {
|
||||
const queryClient = makeTestQueryClient();
|
||||
const outbound = {
|
||||
protocol,
|
||||
tag: 'out',
|
||||
settings: {
|
||||
vnext: [
|
||||
{
|
||||
address: 'example.com',
|
||||
port: 443,
|
||||
users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
streamSettings: { network: 'tcp', security: 'none' },
|
||||
};
|
||||
const tree = (open: boolean) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<OutboundFormModal
|
||||
open={open}
|
||||
outbound={outbound}
|
||||
existingTags={[]}
|
||||
onClose={() => {}}
|
||||
onConfirm={() => {}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const view = render(tree(false));
|
||||
view.rerender(tree(true));
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
const select = formItemByLabel('Transmission')?.querySelector('.ant-select');
|
||||
if (!select) throw new Error('Transmission select not found');
|
||||
fireEvent.mouseDown(select.querySelector('.ant-select-selector') ?? select);
|
||||
const options = Array.from(document.querySelectorAll('.ant-select-item-option')).map((o) =>
|
||||
(o.getAttribute('title') ?? o.textContent ?? '').trim(),
|
||||
);
|
||||
view.unmount();
|
||||
return options;
|
||||
};
|
||||
|
||||
expect(await transmissionOptions('vmess')).not.toContain('XDRIVE');
|
||||
expect(await transmissionOptions('vless')).toContain('XDRIVE');
|
||||
});
|
||||
|
||||
it('saves an XDRIVE stream picked in the transmission selector', async () => {
|
||||
@@ -421,15 +483,83 @@ describe('OutboundFormModal', () => {
|
||||
});
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||
const payload = onConfirm.mock.calls[0][0] as { streamSettings: Record<string, unknown> };
|
||||
const payload = onConfirm.mock.calls[0][0] as {
|
||||
streamSettings: Record<string, unknown>;
|
||||
mux?: Record<string, unknown>;
|
||||
};
|
||||
expect(payload.streamSettings).toMatchObject({
|
||||
network: 'xdrive',
|
||||
xdriveSettings: {
|
||||
service: 'Google Drive',
|
||||
service: 'google-drive',
|
||||
remoteFolder: 'xray-tunnel',
|
||||
secrets: ['id.apps.googleusercontent.com', 'client-secret', 'refresh-token'],
|
||||
},
|
||||
});
|
||||
// xray-core 26.10.17 (#7182) refuses an XDRIVE client without Mux.
|
||||
expect(payload.mux).toMatchObject({ enabled: true });
|
||||
});
|
||||
});
|
||||
|
||||
// xray-core 26.10.17 (#7182) renamed the service "google-drive"; an outbound saved before still
|
||||
// says "Google Drive", which the editor lifts so its secrets stay editable.
|
||||
describe('OutboundFormModal XDRIVE service name', () => {
|
||||
it('opens a stored "Google Drive" outbound under the new service name', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const queryClient = makeTestQueryClient();
|
||||
const outbound = {
|
||||
protocol: 'vless',
|
||||
tag: 'drive-out',
|
||||
settings: {
|
||||
address: 'example.com',
|
||||
port: 443,
|
||||
id: 'c9f0c2d0-0000-4000-8000-000000000000',
|
||||
encryption: 'mlkem768x25519plus.native.0rtt.key',
|
||||
},
|
||||
mux: { enabled: true, concurrency: 8 },
|
||||
streamSettings: {
|
||||
network: 'xdrive',
|
||||
security: 'none',
|
||||
xdriveSettings: {
|
||||
service: 'Google Drive',
|
||||
remoteFolder: 'xray-tunnel',
|
||||
secrets: ['id', 'secret', 'token'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const tree = (open: boolean) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<OutboundFormModal
|
||||
open={open}
|
||||
outbound={outbound}
|
||||
existingTags={[]}
|
||||
onClose={() => {}}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const { rerender } = render(tree(false));
|
||||
rerender(tree(true));
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
expect((screen.getByLabelText('Client ID') as HTMLInputElement).value).toBe('id');
|
||||
const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
|
||||
await act(async () => {
|
||||
fireEvent.click(ok);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||
const payload = onConfirm.mock.calls[0][0] as { streamSettings: Record<string, unknown> };
|
||||
expect(payload.streamSettings.xdriveSettings).toMatchObject({
|
||||
service: 'google-drive',
|
||||
secrets: ['id', 'secret', 'token'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -767,6 +897,33 @@ describe('OutboundFormModal finalmask variables', () => {
|
||||
expect(finalmask.udp).toEqual([{ type: 'header-custom', settings: { client, server: [] } }]);
|
||||
});
|
||||
|
||||
// xray-core 26.10.17 gives noise a Lua `script` and xdns a DoH resolver with its own
|
||||
// worker count, TLS and QUIC settings; the mask editor only keeps keys it registers.
|
||||
it('keeps a noise script and the DoH settings of an xdns resolver on save', async () => {
|
||||
const fixture = {
|
||||
tcp: [],
|
||||
udp: [
|
||||
{ type: 'noise', settings: { reset: 0, script: 'noise.lua' } },
|
||||
{
|
||||
type: 'xdns',
|
||||
settings: {
|
||||
domains: [{ names: ['t.example.com'] }],
|
||||
resolvers: [
|
||||
{
|
||||
addrs: ['https://dns.example.com/dns-query'],
|
||||
worker: 8,
|
||||
tlsConfig: { serverName: 'dns.example.com', fingerprint: 'chrome' },
|
||||
quicConfig: { congestion: 'bbr', maxIdleTimeout: 30 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(await saveFinalmask(structuredClone(fixture))).toEqual(fixture);
|
||||
});
|
||||
|
||||
it('saves a noise reset typed as a range', async () => {
|
||||
const finalmask = await saveFinalmask(
|
||||
{ udp: [{ type: 'noise', settings: { reset: 0, noise: [] } }] },
|
||||
|
||||
@@ -44,3 +44,29 @@ describe('NetworkSettingsSchema method alias', () => {
|
||||
expect((parsed as { network?: string }).network).toBe('grpc');
|
||||
});
|
||||
});
|
||||
|
||||
// xray-core 26.10.17 (#7182) renamed the XDRIVE service, reading "Google Drive" only as an alias.
|
||||
// XDRIVE's and MASQUE's xmux are client keys a JSON subscription hands on from the inbound.
|
||||
describe('NetworkSettingsSchema xray-core 26.10.21 keys', () => {
|
||||
it('folds the "Google Drive" service into google-drive', () => {
|
||||
const parsed = NetworkSettingsSchema.parse({
|
||||
network: 'xdrive',
|
||||
xdriveSettings: { service: 'Google Drive', remoteFolder: 'f', secrets: ['a', 'b', 'c'] },
|
||||
}) as { xdriveSettings: { service: string } };
|
||||
expect(parsed.xdriveSettings.service).toBe('google-drive');
|
||||
});
|
||||
|
||||
it('keeps the XDRIVE and MASQUE xmux', () => {
|
||||
const xmux = { maxConnections: 2, hMaxReusableSecs: '1800-3000' };
|
||||
const xdrive = NetworkSettingsSchema.parse({
|
||||
network: 'xdrive',
|
||||
xdriveSettings: { service: 'local', remoteFolder: '/srv/drive', xmux },
|
||||
}) as { xdriveSettings: { xmux?: unknown } };
|
||||
const masque = NetworkSettingsSchema.parse({
|
||||
network: 'masque',
|
||||
masqueSettings: { host: 'masque.example.com', xmux },
|
||||
}) as { masqueSettings: { xmux?: unknown } };
|
||||
expect(xdrive.xdriveSettings.xmux).toEqual(xmux);
|
||||
expect(masque.masqueSettings.xmux).toEqual(xmux);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user