> For the complete documentation index see https://domscan.net/llms.txt. Markdown versions of every docs page are available by appending .md to the page URL.

Source: https://domscan.net/zh/docs/dns-propagation

# DNS 递归解析器对比

通过 DomScan API，通过独立的 DoH 端点查询两个公共递归 DNS 服务并比较记录响应：Cloudflare 和 Google。 两者都采用全球任播。

通过独立的 DoH 端点查询两个公共递归 DNS 服务并比较记录响应：Cloudflare 和 Google。两者都采用全球任播。结果只是从请求路径观察到的这两个服务的响应，并非地理探针、对权威 DNS 服务器的直接查询，也不能证明 DNS 已在全球传播。

GET /v1/dns/propagation

#### 查询参数

| 参数 | 类型 | 说明 |
| --- | --- | --- |
| domain 必需 | string | 要通过每个已配置解析器查询的域名（例如 "example.com"） |
| type 可选 | string | 要比较的记录类型：A、AAAA、CNAME、MX、TXT、NS、SOA（默认：A） |
| expected 可选 | string | 可选的预期记录值，用作比较基准（例如新的 IP 地址） |

#### 响应字段

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `propagation_percentage` | number | 未提供 expected：最大相同答案组的解析器数量除以全部已配置解析器数量。提供 expected：与预期值完全匹配的解析器数量除以全部已配置解析器数量（0-100） |
| `fully_propagated` | boolean | 当所有已配置解析器返回一致答案，或在提供 expected 时全部匹配该值，结果为 True |
| `consistent` | boolean | 仅当每个已配置解析器都成功响应并返回相同的规范化答案集时，结果为 True |
| `unique_values` | array | 成功响应的解析器返回的所有唯一记录值 |
| `results` | array | 每个解析器的提供商、记录、观测 TTL、从请求路径测得的响应时间和错误 |

#### 请求示例

```bash
# Compare A records across configured resolvers
curl -H "X-API-Key: your-api-key" "https://domscan.net/v1/dns/propagation?domain=example.com&type=A"

# Compare MX answers against an expected value
curl -H "X-API-Key: your-api-key" "https://domscan.net/v1/dns/propagation?domain=example.com&type=MX&expected=mail.example.com"
```

```javascript
const domscanFetch = (url, options = {}) =>
  fetch(url, {
    ...options,
    headers: { ...options.headers, "X-API-Key": "your-api-key" },
  });

const url = new URL("https://domscan.net/v1/dns/propagation");
url.searchParams.set("domain", "example.com");
url.searchParams.set("type", "A");
url.searchParams.set("expected", "203.0.113.42");

const response = await domscanFetch(url);
const data = await response.json();

// Legacy response field names are retained for API compatibility.
console.log(`Resolver convergence: ${data.propagation_percentage}%`);
console.log(`All configured resolvers match: ${data.fully_propagated}`);

// Check which servers are still showing old values
data.results
  .filter(r => !r.success || !r.records.includes(data.expected_value))
  .forEach(r => console.log(`${r.server.name}: ${r.records}`));
```

```python
import requests

domscan = requests.Session()
domscan.headers.update({"X-API-Key": "your-api-key"})

response = domscan.get(
    "https://domscan.net/v1/dns/propagation",
    params={"domain": "example.com", "type": "A"}
)
data = response.json()

# Legacy response field names are retained for API compatibility.
print(f"Resolver convergence: {data['propagation_percentage']}%")
print(f"All configured resolvers match: {data['fully_propagated']}")

# Show servers with different values
for result in data['results']:
    print(f"{result['server']['name']}: {result['records']}")
```

```go
package main

import (
  "encoding/json"
  "fmt"
  "net/http"
  "os"
)

func domscanGet(url string) (*http.Response, error) {
  request, err := http.NewRequest(http.MethodGet, url, nil)
  if err != nil {
    return nil, err
  }
  request.Header.Set("X-API-Key", os.Getenv("DOMSCAN_API_KEY"))
  return http.DefaultClient.Do(request)
}

func main() {
  resp, _ := domscanGet("https://domscan.net/v1/dns/propagation?domain=example.com&type=A")
  defer resp.Body.Close()

  var data map[string]interface{}
  json.NewDecoder(resp.Body).Decode(&data)

  // Legacy response field names are retained for API compatibility.
  fmt.Printf("Resolver convergence: %.0f%%\n", data["propagation_percentage"])
  fmt.Printf("All configured resolvers match: %v\n", data["fully_propagated"])
}
```

```ruby
require 'net/http'
require 'json'

uri = URI("https://domscan.net/v1/dns/propagation?domain=example.com&type=A")
request = Net::HTTP::Get.new(uri)
request["X-API-Key"] = ENV.fetch("DOMSCAN_API_KEY")
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
data = JSON.parse(response.body)

# Legacy response field names are retained for API compatibility.
puts "Resolver convergence: #{data['propagation_percentage']}%"
puts "All configured resolvers match: #{data['fully_propagated']}"
```

#### 响应示例

```json
{
  "domain": "example.com",
  "record_type": "A",
  "measurement_scope": "configured_recursive_resolvers",
  "percentage_basis": "resolver_convergence",
  "propagation_percentage": 100,
  "fully_propagated": true,
  "consistent": true,
  "unique_values": ["93.184.216.34"],
  "results": [
    {
      "server": {
        "name": "Cloudflare 1.1.1.1",
        "ip": "1.1.1.1",
        "provider": "Cloudflare",
        "location": "Global anycast",
        "country": "GLOBAL",
        "scope": "global-anycast",
        "doh_endpoint": "https://cloudflare-dns.com/dns-query"
      },
      "success": true,
      "records": ["93.184.216.34"],
      "ttl": 86400,
      "response_time_ms": 12
    },
    {
      "server": {
        "name": "Google Public DNS",
        "ip": "8.8.8.8",
        "provider": "Google",
        "location": "Global anycast",
        "country": "GLOBAL",
        "scope": "global-anycast",
        "doh_endpoint": "https://dns.google/resolve"
      },
      "success": true,
      "records": ["93.184.216.34"],
      "ttl": 86400,
      "response_time_ms": 15
    }
  ],
  "summary": {
    "total_servers": 2,
    "successful": 2,
    "failed": 0
  }
}
```

GET /v1/dns/servers

列出用于比较的两个公共递归 DoH 服务：Cloudflare 和 Google。两者均采用全球任播，不是地理探针，也不是权威 DNS 服务器。

#### 响应示例

```json
{
  "measurement_scope": "configured_recursive_resolvers",
  "geographic_vantage": false,
  "servers": [
    {
      "name": "Cloudflare 1.1.1.1",
      "ip": "1.1.1.1",
      "provider": "Cloudflare",
      "location": "Global anycast",
      "country": "GLOBAL",
      "scope": "global-anycast",
      "doh_endpoint": "https://cloudflare-dns.com/dns-query"
    },
    {
      "name": "Google Public DNS",
      "ip": "8.8.8.8",
      "provider": "Google",
      "location": "Global anycast",
      "country": "GLOBAL",
      "scope": "global-anycast",
      "doh_endpoint": "https://dns.google/resolve"
    }
  ],
  "total": 2
}
```

#### 响应字段

| 字段 | 类型 |
| --- | --- |
| `measurement_scope` | string |
| `geographic_vantage` | boolean |
| `servers[]` | object[] |
| `servers[]` | object |
| `servers[].name` | string |
| `servers[].location` | string |
| `servers[].country` | string |
| `servers[].ip` | string |
| `servers[].provider` | string |
| `servers[].scope` | string |
| `servers[].doh_endpoint` | string |
| `servers[].documentation_url` | string |
| `servers[].format` | string |
| `total` | integer |
| `meta` | object |
| `meta.served_by` | string |

POST /v1/dns/propagation/bulk 积分: 2/item

#### 查询参数

| 参数 | 类型 | 必需 |
| --- | --- | --- |
| dry_run | string 允许的值 `true` | 可选 |

#### 正文参数

| 参数 | 类型 | 必需 |
| --- | --- | --- |
| domains | string[] | 必需 |
| type | string 允许的值 `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SOA` 默认值 `A` | 可选 |
| expected | string | 可选 |

#### 响应字段

| 字段 | 类型 |
| --- | --- |
| `results[]` | unknown[] |
| `meta` | object |
| `meta.total` | integer |
| `meta.succeeded` | integer |
| `meta.failed` | integer |
| `meta.max_items` | integer |
| `meta.credits_per_item` | integer |
| `meta.duration_ms` | integer |

#### 请求示例

```bash
curl -X POST "https://domscan.net/v1/dns/propagation/bulk" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $DOMSCAN_API_KEY" \
  -d '{
  "domains": [
    "example.com",
    "cloudflare.com"
  ],
  "type": "A"
}'
```

```javascript
const response = await fetch("https://domscan.net/v1/dns/propagation/bulk", {
  method: "POST",
  headers: { "X-API-Key": process.env.DOMSCAN_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    "domains": [
      "example.com",
      "cloudflare.com"
    ],
    "type": "A"
  }),
});
const data = await response.json();
console.log(data);
```

```python
import os
import requests

response = requests.post(
    "https://domscan.net/v1/dns/propagation/bulk",
    headers={"X-API-Key": os.environ["DOMSCAN_API_KEY"]},
    json={
        "domains": [
            "example.com",
            "cloudflare.com",
        ],
        "type": "A",
    },
)
print(response.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
	  "domains": [
	    "example.com",
	    "cloudflare.com"
	  ],
	  "type": "A"
	}`)
	req, err := http.NewRequest(http.MethodPost, "https://domscan.net/v1/dns/propagation/bulk", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-API-Key", os.Getenv("DOMSCAN_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.Status, string(data))
}
```

```ruby
require "json"
require "net/http"

uri = URI("https://domscan.net/v1/dns/propagation/bulk")
request = Net::HTTP::Post.new(uri)
request["X-API-Key"] = ENV.fetch("DOMSCAN_API_KEY")
request["Content-Type"] = "application/json"
request.body = JSON.generate({
  "domains" => [
    "example.com",
    "cloudflare.com"
  ],
  "type" => "A"
})

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end
puts JSON.pretty_generate(JSON.parse(response.body))
```

#### 响应示例

```json
{
  "results": [
    {
      "input": "example",
      "data": {}
    }
  ],
  "meta": {
    "total": 1,
    "succeeded": 1,
    "failed": 1,
    "max_items": 10,
    "credits_per_item": 1,
    "duration_ms": 1
  }
}
```

#### HTTP 状态码

- `200` 成功
- `400` 请求错误
- `401` 未授权
- `402` 需要付款
- `409` HTTP 409
- `422` HTTP 422
- `429` 请求过多
