fix(xray): stop a lone dns qType 0 from matching every query

The core reads a dns rule's qType as a PortList, which drops a bare numeric
0 (infra/conf/common.go: `if number != 0`), and a rule with no qTypes
matches every query. A stored `"qType": 0` therefore does not target query
type 0: it drops, refuses or hijacks all DNS through that outbound.

A qType the panel writes has to be read by the core as exactly the query
types it names. Four writers broke that:

- DNSOutboundLegacyKeysFix rewrote a lone blockTypes [0] into "qType": 0,
  so "block type 0" became "block everything" on upgrade.
- That seeder shipped in v3.8.0 and is recorded as done, so fixing it does
  not reach installs that already ran it. DNSOutboundQTypeZeroFix spells
  any stored numeric qType 0 as "0" once, protocol id matched like the core.
- The outbound form adapter turned a typed "0" into the number 0.
- The Xray template editor saves raw JSON past that adapter; the save now
  applies the same rewrite.

Each writer is pinned by a test that fails without its part. The rewrite
and the repair compare policies as the pinned core builds them, and the
repair runs through runSeeders over a database whose legacy-keys seeder
already ran, on SQLite and PostgreSQL 16.
This commit is contained in:
Sanaei
2026-09-15 15:45:01 +02:00
parent ac3fc12077
commit bc424f0968
6 changed files with 231 additions and 4 deletions
@@ -786,7 +786,8 @@ function dnsRuleToWire(r: DnsRuleForm) {
const result: Raw = { action };
const qType = r.qType.trim();
if (qType) {
result.qType = /^\d+$/.test(qType) ? Number(qType) : qType;
// The core reads a numeric 0 as no qType at all, which matches every query.
result.qType = /^\d+$/.test(qType) && Number(qType) > 0 ? Number(qType) : qType;
}
const domains = r.domain
.split(',')