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(); + }); +});