mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
d5ab84e8d5
* feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { formValuesToWirePayload, rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
|
|
import type { AmneziaWGOutboundFormSettings } from '@/schemas/forms/outbound-form';
|
|
|
|
// amneziawg outbound: lossless form->wire->form; payload stays a raw row.
|
|
describe('amneziawg outbound adapter', () => {
|
|
const wire = {
|
|
mtu: 1380,
|
|
secretKey: '6Nn0ZB4C1Pj3TBEsXgLv7VdmSnYXGxS+HhVBDhvGgHE=',
|
|
address: ['10.8.0.2/32'],
|
|
listenPort: 40001,
|
|
jc: 5,
|
|
jmin: 40,
|
|
jmax: 90,
|
|
s1: 20,
|
|
s2: 90,
|
|
s3: 15,
|
|
s4: 13,
|
|
h1: '100-800',
|
|
h2: '900-1600',
|
|
h3: '1700-2400',
|
|
h4: '2500-3200',
|
|
i1: '<r 64>',
|
|
contentPaddingAddition: '8-40',
|
|
randomTrailers: true,
|
|
disableCookies: false,
|
|
peers: [
|
|
{
|
|
publicKey: 'Qk9fWqDqC7LzKpYvJq0m2b1tq8eF3uY6oPpRrSsTtUu=',
|
|
presharedKey: 'cHNo',
|
|
allowedIPs: ['0.0.0.0/0', '::/0'],
|
|
endpoint: '203.0.113.7:51820',
|
|
keepAlive: 25,
|
|
},
|
|
],
|
|
};
|
|
|
|
it('hydrates defaults when the template omits optional keys', () => {
|
|
const values = rawOutboundToFormValues({ protocol: 'amneziawg', tag: 'awg-x' });
|
|
expect(values.protocol).toBe('amneziawg');
|
|
const s = values.settings as AmneziaWGOutboundFormSettings;
|
|
expect(s.mtu).toBe(0);
|
|
expect(s.randomTrailers).toBe(false);
|
|
expect(s.disableCookies).toBe(true);
|
|
expect(s.peers).toEqual([]);
|
|
expect(values.tag).toBe('awg-x');
|
|
});
|
|
|
|
// A blank MTU must reach the backend absent, not pinned to 1420: the Go
|
|
// EffectiveMTU subtracts S4 from the default only when the field is unset.
|
|
it('leaves a defaulted MTU out of the payload so the backend derives it', () => {
|
|
const values = rawOutboundToFormValues({ protocol: 'amneziawg', tag: 'awg-x' });
|
|
const payload = formValuesToWirePayload(values);
|
|
expect((payload.settings as Record<string, unknown>).mtu).toBeUndefined();
|
|
});
|
|
|
|
it('round-trips wire -> form -> wire losslessly', () => {
|
|
const values = rawOutboundToFormValues({ protocol: 'amneziawg', tag: 'awg-x', settings: wire });
|
|
const payload = formValuesToWirePayload(values);
|
|
expect(payload.protocol).toBe('amneziawg');
|
|
expect(payload.tag).toBe('awg-x');
|
|
// undefined-valued optionals are dropped by JSON semantics; compare the
|
|
// meaningful fields directly.
|
|
expect((payload.settings as Record<string, unknown>).mtu).toBe(1380);
|
|
expect((payload.settings as Record<string, unknown>).listenPort).toBe(40001);
|
|
expect((payload.settings as Record<string, unknown>).i1).toBe('<r 64>');
|
|
expect((payload.settings as Record<string, unknown>).peers).toEqual(wire.peers);
|
|
expect((payload.settings as Record<string, unknown>).disableCookies).toBe(false);
|
|
});
|
|
|
|
it('omits empty optional strings and zero listenPort from the payload', () => {
|
|
const values = rawOutboundToFormValues({ protocol: 'amneziawg', settings: wire });
|
|
const awg = values.settings as AmneziaWGOutboundFormSettings;
|
|
awg.i1 = '';
|
|
awg.listenPort = 0;
|
|
const payload = formValuesToWirePayload(values);
|
|
const s = payload.settings as Record<string, unknown>;
|
|
expect(s.i1).toBeUndefined();
|
|
expect(s.listenPort).toBeUndefined();
|
|
// always-present booleans survive so a true->false edit is diffable
|
|
expect(s.randomTrailers).toBe(true);
|
|
});
|
|
|
|
it('is included in every protocol-capability gate like wireguard (non-stream, non-mux)', () => {
|
|
const values = rawOutboundToFormValues({
|
|
protocol: 'amneziawg',
|
|
settings: wire,
|
|
streamSettings: { network: 'tcp', tcpSettings: {} },
|
|
});
|
|
const payload = formValuesToWirePayload(values);
|
|
// Non-stream protocol keeps only sockopt; here there is none.
|
|
expect(payload.streamSettings).toBeUndefined();
|
|
});
|
|
});
|