From c392f367e12b9956c0793e5868bd67db0b14a22c Mon Sep 17 00:00:00 2001 From: Sanaei Date: Wed, 9 Sep 2026 00:47:18 +0200 Subject: [PATCH] fix(dns): stop offering a port field that DoH entries discard Xray ignores port for DoH/DoHL/DoQL, so valuesToWire deliberately stores none for an encrypted address and a non-standard port has to go inside the URL. The form kept offering the field anyway, pre-filled with the 53 from its own defaults: a port typed there was dropped on save and redrawn as 53 on reopen, which reads as the panel losing the value. Render the port field only where it is actually stored. DoT keeps it, since tls:// is not an encrypted-address scheme for this purpose. Closes #6403 --- .../src/pages/xray/dns/DnsServerModal.tsx | 24 ++++++---- .../src/test/dns-server-port-field.test.tsx | 47 +++++++++++++++++++ 2 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 frontend/src/test/dns-server-port-field.test.tsx diff --git a/frontend/src/pages/xray/dns/DnsServerModal.tsx b/frontend/src/pages/xray/dns/DnsServerModal.tsx index f1b75fb35..d6899ea14 100644 --- a/frontend/src/pages/xray/dns/DnsServerModal.tsx +++ b/frontend/src/pages/xray/dns/DnsServerModal.tsx @@ -137,7 +137,13 @@ export default function DnsServerModal({ onConfirm, }: DnsServerModalProps) { const { t } = useTranslation(); - const methods = useForm({ defaultValues: defaultFormValues() }); + const methods = useForm({ + defaultValues: defaultFormValues(), + }); + const address = useWatch({ control: methods.control, name: 'address' }) ?? ''; + // Xray ignores port for DoH/DoHL/DoQL, so valuesToWire never stores one: + // offering the field there discards whatever is typed into it. + const portApplies = !isEncryptedDnsAddress(address); const domains = useWatch({ control: methods.control, name: 'domains' }) ?? []; const expectedIPs = useWatch({ control: methods.control, name: 'expectedIPs' }) ?? []; const unexpectedIPs = useWatch({ control: methods.control, name: 'unexpectedIPs' }) ?? []; @@ -168,13 +174,15 @@ export default function DnsServerModal({ > - - - + {portApplies && ( + + + + )} diff --git a/frontend/src/test/dns-server-port-field.test.tsx b/frontend/src/test/dns-server-port-field.test.tsx new file mode 100644 index 000000000..750be3a6e --- /dev/null +++ b/frontend/src/test/dns-server-port-field.test.tsx @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { fireEvent, screen } from '@testing-library/react'; + +import DnsServerModal from '@/pages/xray/dns/DnsServerModal'; +import { renderWithProviders } from './test-utils'; + +describe('DnsServerModal port field', () => { + it('hides the port for an encrypted address, whose port lives in the URL', () => { + renderWithProviders( + {}} + onConfirm={() => {}} + />, + ); + + expect(screen.queryByLabelText('Port')).toBeNull(); + }); + + it('offers the port for a plain address and for DoT', () => { + renderWithProviders( + {}} + onConfirm={() => {}} + />, + ); + + expect(screen.getByLabelText('Port')).toBeTruthy(); + }); + + it('drops the port field as soon as the address becomes a DoH URL', () => { + renderWithProviders( + {}} onConfirm={() => {}} />, + ); + + expect(screen.getByLabelText('Port')).toBeTruthy(); + fireEvent.change(screen.getByLabelText('Address'), { + target: { value: 'https://dns.example.com/dns-query' }, + }); + expect(screen.queryByLabelText('Port')).toBeNull(); + }); +});