1
0

feat: DNS checker,自研 codec/transport,支持 system/server 双模式,UDP/TCP + TC fallback

This commit is contained in:
2026-05-24 17:06:22 +08:00
parent 4f33fba793
commit 483cdc596b
21 changed files with 5686 additions and 16 deletions

View File

@@ -0,0 +1,473 @@
import { describe, expect, it } from "bun:test";
import type { CheckerValidationInput } from "../../../../../src/server/checker/runner/types";
import { validateDnsConfig } from "../../../../../src/server/checker/runner/dns/validate";
function makeInput(overrides: { targets?: Array<Record<string, unknown>> }): CheckerValidationInput {
return { targets: (overrides.targets ?? []) as CheckerValidationInput["targets"] };
}
describe("validateDnsConfig", () => {
it("接受合法的 system 目标resolver/name/family", () => {
const issues = validateDnsConfig(
makeInput({
targets: [{ dns: { family: "ipv4", name: "example.com", resolver: "system" }, id: "t1", type: "dns" }],
}),
);
expect(issues).toHaveLength(0);
});
it("接受合法的 server 目标resolver/name/server/port/recordType", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: {
name: "example.com",
port: 53,
protocol: "udp",
recordType: "A",
resolver: "server",
server: "8.8.8.8",
},
id: "t2",
type: "dns",
},
],
}),
);
expect(issues).toHaveLength(0);
});
it("拒绝缺少 dns 配置分组", () => {
const issues = validateDnsConfig(makeInput({ targets: [{ id: "t3", type: "dns" }] }));
expect(issues).toHaveLength(1);
expect(issues[0]!.code).toBe("required");
expect(issues[0]!.path).toContain("dns");
expect(issues[0]!.message).toContain("dns");
});
it("拒绝缺少 dns.resolver", () => {
const issues = validateDnsConfig(makeInput({ targets: [{ dns: { name: "example.com" }, id: "t4", type: "dns" }] }));
expect(issues).toHaveLength(1);
expect(issues[0]!.code).toBe("required");
expect(issues[0]!.path).toContain("resolver");
});
it("拒绝无效的 dns.resolver 值", () => {
const issues = validateDnsConfig(
makeInput({ targets: [{ dns: { name: "example.com", resolver: "unknown" }, id: "t5", type: "dns" }] }),
);
expect(issues).toHaveLength(1);
expect(issues[0]!.code).toBe("invalid-value");
expect(issues[0]!.path).toContain("resolver");
expect(issues[0]!.message).toContain("system");
expect(issues[0]!.message).toContain("server");
});
it("拒绝缺少 dns.name", () => {
const issues = validateDnsConfig(makeInput({ targets: [{ dns: { resolver: "system" }, id: "t6", type: "dns" }] }));
expect(issues.length).toBeGreaterThanOrEqual(1);
expect(issues.some((i) => i.code === "required" && i.path.includes("name"))).toBe(true);
});
it("System 模式:拒绝空白 dns.name", () => {
const issues = validateDnsConfig(
makeInput({ targets: [{ dns: { name: " ", resolver: "system" }, id: "t7", type: "dns" }] }),
);
expect(issues.some((i) => i.code === "required" && i.path.includes("name"))).toBe(true);
});
it("System 模式:接受 family any/ipv4/ipv6拒绝无效 family", () => {
for (const family of ["any", "ipv4", "ipv6"]) {
const issues = validateDnsConfig(
makeInput({ targets: [{ dns: { family, name: "x.com", resolver: "system" }, id: "tf", type: "dns" }] }),
);
expect(issues).toHaveLength(0);
}
const issues = validateDnsConfig(
makeInput({ targets: [{ dns: { family: "ipx", name: "x.com", resolver: "system" }, id: "tf", type: "dns" }] }),
);
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("family"))).toBe(true);
});
it("System 模式:拒绝 dns 中的未知字段", () => {
const issues = validateDnsConfig(
makeInput({
targets: [{ dns: { bogus: true, name: "x.com", resolver: "system" }, id: "t8", type: "dns" }],
}),
);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("bogus"))).toBe(true);
});
it("System 模式:拒绝 server 专用字段server/port/protocol/recordType", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", port: 53, protocol: "udp", recordType: "A", resolver: "system", server: "8.8.8.8" },
id: "t9",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("server"))).toBe(true);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("port"))).toBe(true);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("protocol"))).toBe(true);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("recordType"))).toBe(true);
});
it("Server 模式:拒绝缺少 dns.server", () => {
const issues = validateDnsConfig(
makeInput({ targets: [{ dns: { name: "x.com", resolver: "server" }, id: "t10", type: "dns" }] }),
);
expect(issues.some((i) => i.code === "required" && i.path.includes("server"))).toBe(true);
});
it("Server 模式:拒绝无效 port0、-1、65536、1.5", () => {
for (const port of [0, -1, 65536, 1.5]) {
const issues = validateDnsConfig(
makeInput({
targets: [{ dns: { name: "x.com", port, resolver: "server", server: "8.8.8.8" }, id: "tp", type: "dns" }],
}),
);
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("port"))).toBe(true);
}
});
it("Server 模式:接受 protocol udp/tcp拒绝无效值", () => {
for (const protocol of ["udp", "tcp"]) {
const issues = validateDnsConfig(
makeInput({
targets: [
{ dns: { name: "x.com", protocol, resolver: "server", server: "8.8.8.8" }, id: "tpr", type: "dns" },
],
}),
);
expect(issues).toHaveLength(0);
}
const issues = validateDnsConfig(
makeInput({
targets: [
{ dns: { name: "x.com", protocol: "http", resolver: "server", server: "8.8.8.8" }, id: "tpr", type: "dns" },
],
}),
);
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("protocol"))).toBe(true);
});
it("Server 模式:接受有效 recordType拒绝无效 recordType", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{ dns: { name: "x.com", recordType: "A", resolver: "server", server: "8.8.8.8" }, id: "trt", type: "dns" },
],
}),
);
expect(issues).toHaveLength(0);
const badIssues = validateDnsConfig(
makeInput({
targets: [
{ dns: { name: "x.com", recordType: "FAKE", resolver: "server", server: "8.8.8.8" }, id: "trt", type: "dns" },
],
}),
);
expect(badIssues.some((i) => i.code === "invalid-value" && i.path.includes("recordType"))).toBe(true);
});
it("Server 模式:接受有效 maxResponseBytes数字和字符串", () => {
const issues1 = validateDnsConfig(
makeInput({
targets: [
{
dns: { maxResponseBytes: 512, name: "x.com", resolver: "server", server: "8.8.8.8" },
id: "tmr",
type: "dns",
},
],
}),
);
expect(issues1).toHaveLength(0);
const issues2 = validateDnsConfig(
makeInput({
targets: [
{
dns: { maxResponseBytes: "1KB", name: "x.com", resolver: "server", server: "8.8.8.8" },
id: "tmr",
type: "dns",
},
],
}),
);
expect(issues2).toHaveLength(0);
});
it("Server 模式:拒绝无效 maxResponseBytes 字符串", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { maxResponseBytes: "1kb", name: "x.com", resolver: "server", server: "8.8.8.8" },
id: "tmr-invalid",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("maxResponseBytes"))).toBe(true);
});
it("Server 模式:拒绝 recursionDesired/tcpFallback 非布尔值", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: {
name: "x.com",
recursionDesired: "false",
resolver: "server",
server: "8.8.8.8",
tcpFallback: "true",
},
id: "tb-dns",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("recursionDesired"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("tcpFallback"))).toBe(true);
});
it("Server 模式:拒绝 dns 中的未知字段", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{ dns: { name: "x.com", resolver: "server", server: "8.8.8.8", unknown: true }, id: "t16", type: "dns" },
],
}),
);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("unknown"))).toBe(true);
});
it("System 模式 expect拒绝 rcode/ttlMin/ttlMax/answerCount/authoritative/recursionAvailable/truncated/authenticatedData/result/responded → dns-unsupported-expect", () => {
const serverOnlyFields = [
"rcode",
"ttlMin",
"ttlMax",
"answerCount",
"authoritative",
"recursionAvailable",
"truncated",
"authenticatedData",
"result",
"responded",
];
for (const field of serverOnlyFields) {
const expectObj: Record<string, unknown> = {};
expectObj[field] = field === "rcode" ? ["NOERROR"] : field === "result" ? ["ok"] : true;
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "system" },
expect: expectObj,
id: "tse",
name: "sys-target",
type: "dns",
},
],
}),
);
const matched = issues.find((i) => i.code === "dns-unsupported-expect" && i.path.includes(field));
expect(matched).toBeDefined();
expect(matched!.message).toContain("system");
}
});
it("System 模式 expect接受 values/valueCount/durationMs", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "system" },
expect: { durationMs: { lte: 100 }, valueCount: { gte: 1 }, values: { exact: ["1.2.3.4"] } },
id: "t18",
type: "dns",
},
],
}),
);
expect(issues).toHaveLength(0);
});
it("Server 模式 expect接受所有 server expect 字段", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: {
answerCount: { gte: 1 },
authenticatedData: false,
authoritative: false,
durationMs: { lte: 100 },
rcode: ["NOERROR"],
recursionAvailable: true,
responded: true,
result: [{ contains: "ok" }],
truncated: false,
ttlMax: { lte: 3600 },
ttlMin: { gte: 0 },
valueCount: { gte: 1 },
values: { exact: ["1.2.3.4"] },
},
id: "t19",
type: "dns",
},
],
}),
);
expect(issues).toHaveLength(0);
});
it("Server 模式 expectresponded=false 时拒绝协议级响应断言", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: { rcode: ["NOERROR"], responded: false },
id: "trf",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("responded"))).toBe(true);
});
it("Server 模式 expect拒绝未知 expect 字段", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: { bogusField: 123 },
id: "t20",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("bogusField"))).toBe(true);
});
it("验证 rcode 值必须为已知 RCODE", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: { rcode: ["NOTAREALCODE"] },
id: "trc",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("rcode"))).toBe(true);
});
it("验证布尔字段responded/authoritative 等)", () => {
const fields = ["responded", "authoritative", "recursionAvailable", "truncated", "authenticatedData"];
for (const field of fields) {
const expectObj: Record<string, unknown> = {};
expectObj[field] = "notbool";
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: expectObj,
id: "tb",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes(field))).toBe(true);
}
});
it("验证 ValueExpectation 字段durationMs/valueCount/answerCount/ttlMin/ttlMax", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: { answerCount: [4], durationMs: [1, 2], ttlMax: [6], ttlMin: [5], valueCount: [3] },
id: "tve",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("durationMs"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("valueCount"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("answerCount"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("ttlMin"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("ttlMax"))).toBe(true);
});
it("验证 ContentExpectations 字段result", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: { result: "not-array" },
id: "tce",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("result"))).toBe(true);
});
it("验证 DnsValuesExpectationexact/include/exclude 数组)", () => {
const issues = validateDnsConfig(
makeInput({
targets: [
{
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
expect: { values: { exact: "not-array", exclude: true, include: 123, unknownKey: "x" } },
id: "tdv",
type: "dns",
},
],
}),
);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("exact"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("include"))).toBe(true);
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("exclude"))).toBe(true);
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("unknownKey"))).toBe(true);
});
it("跳过非 dns 类型目标", () => {
const issues = validateDnsConfig(
makeInput({ targets: [{ http: { url: "http://example.com" }, id: "tother", type: "http" }] }),
);
expect(issues).toHaveLength(0);
});
it("跳过非对象目标", () => {
const issues = validateDnsConfig(makeInput({ targets: ["not-an-object" as unknown as Record<string, unknown>] }));
expect(issues).toHaveLength(0);
});
});