一个自动分类的DNS服务器

25 年 12 月 15 日 星期一 (已编辑)
1786 字
9 分钟

所有代码都有claude编写 未提供产物,需要自行从源码构建 所有的功能都是以自己的需求为准,想用但觉得不好用欢迎pr或fork

简介

自动 DNS 代理服务器,支持域名分类、GeoIP 路由、ECS、多级缓存和自动回退。

特性

  • 自动路由 - 对未分类的域名自动进行分类,记录
  • GeoIP/ASN 匹配 - IP 验证和地理位置判断
  • CNAME 链缓存 - RR 级别缓存,支持 CNAME 链部分命中
  • ECS 支持 - EDNS Client Subnet,可按组配置
  • 并发回退 - proxy_ecs_fallback 策略并发查询多个上游,自动选择最优结果
  • 多级缓存 - DNS 缓存和域名分类缓存,支持 Redis 和内存两种后端
  • 代理支持 - 上游 DNS 和文件下载支持 SOCKS5 代理
  • 自动更新 - 定时更新域名分类和 GeoIP 数据库
  • 高性能 - Singleflight 去重,连接池复用

核心

Pasted image 20251215001418.png

域名分类

域名分类存储在 dlc.dat(来自 v2ray/domain-list-community),支持三种匹配方式:

  • 完整匹配 - example.com 只匹配 example.com
  • 域名匹配 - domain:example.com 匹配 example.com 和所有子域名
  • 关键字匹配 - keyword:google 匹配包含 google 的所有域名

分类缓存支持两种模式:

  1. 按需查询(默认)- 查询时从 dlc.dat 读取,结果缓存到 Redis/内存
  2. 预加载-load 模式)- 启动时将所有分类加载到 Redis

查询策略

每个域名分类对应一个查询策略,策略指定:

  • 上游组 - 使用哪个 upstream_group
  • 策略选项 - disable_cache, disable_https, ecs, expected_ips, fallback_group 等

特殊策略

block - 阻断域名,返回指定类型的响应:

  • nxdomain - NXDOMAIN(域名不存在)
  • noerror - NOERROR(空响应)
  • 0.0.0.0 - 返回 0.0.0.0

proxy_ecs_fallback - 并发查询策略(未分类域名的默认策略):

  1. 并发查询 proxy_ecs 和 proxy 组
  2. 检查 proxy_ecs 结果的 IP 是否匹配 fallback.rule(GeoIP 规则)
  3. 如果匹配,查询 direct 组并返回
  4. 否则返回 proxy 或 proxy_ecs 结果
  5. 自动将域名分类为 direct_site 或 proxy_site

IP 验证与回退

策略可以配置 expected_ips(GeoIP 规则数组),验证上游返回的 IP:

query_policy:
  - name: "cn_site"
    group: "proxy"
    options:
      expected_ips: ["geoip:cn"]      # 期望返回国内 IP
      fallback_group: "direct"        # 如果不是国内 IP,使用 direct 组重查

验证逻辑:

  • 所有返回的 IP 必须匹配 expected_ips 中的任一规则
  • 如果验证失败且配置了 fallback_group,使用该组重新查询(最终结果,不再验证)
  • 如果没有配置 fallback_group,回退到 proxy_ecs_fallback 策略

GeoIP 规则

支持的规则格式:

  • geoip:cn - 国家代码(ISO 3166-1 alpha-2)
  • geoip:private - 私有 IP(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • asn:13335 - ASN 号(自治系统号)

CNAME 链缓存

DNS 缓存按 RR 记录级别存储(不是完整的 DNS 消息),支持 CNAME 链部分命中:

示例: a.com -> b.com -> c.com -> 1.2.3.4

  1. 第一次查询 a.com,缓存所有 RR:

    • a.com CNAME b.com
    • b.com CNAME c.com
    • c.com A 1.2.3.4
  2. c.com A 记录过期后再次查询 a.com

    • 从缓存返回 a.com CNAME b.com 和 b.com CNAME c.com
    • 只查询上游 c.com A
    • 合并结果返回

ECS(EDNS Client Subnet)

ECS 在上游组级别配置:

upstream_group:
  proxy_ecs:
    nameservers: ["https://dns.google/dns-query"]
    ecs_ip: "8.8.8.8"              # 固定 ECS IP
  proxy_ecs_auto:
    nameservers: ["https://dns.google/dns-query"]
    ecs_ip: ""                     # 使用全局默认 ECS(如果启用)

ecs:
  enable: true                     # 全局 ECS 开关
  default_ipv4: "8.8.8.8"          # 默认 IPv4 ECS
  default_ipv6: "2001:4860:4860::8888"
  ipv4_prefix: 24                  # ECS 前缀长度
  ipv6_prefix: 48

缓存

DNS 缓存

RR 级别缓存,每条记录独立存储,包含:

  • RR 记录内容
  • 原始 TTL 和存储时间
  • Rcode, AD, RA 标志

支持后端:

  • Redis - 跨进程共享,持久化
  • Memory - 进程内存,重启丢失

最大 TTL 固定为 24 小时。

域名分类缓存

存储域名到分类的映射(如 google.com -> proxy_site)。

支持后端:

  • Redis - TTL 可配置(默认 1 天)
  • Memory - 无 TTL 限制

代理支持

支持通过 SOCKS5 代理进行:

  • 上游 DNS 查询 - DoH (DNS-over-HTTPS) 和 TCP 协议
  • 文件下载 - dlc.dat, Country.mmdb, GeoLite2-ASN.mmdb

每个上游组可以指定不同的 outbound:

upstream_group:
  direct:
    nameservers: ["223.5.5.5"]
    outbound: "direct"             # 不使用代理
  proxy:
    nameservers: ["https://dns.google/dns-query"]
    outbound: "proxy"              # 使用 SOCKS5 代理

outbound:
  - tag: "proxy"
    type: "socks5"
    enable: true
    server: "127.0.0.1"
    port: 1080
    username: ""                   # 可选
    password: ""                   # 可选
  - tag: "file_download"           # 文件下载专用代理
    type: "socks5"
    enable: true
    server: "127.0.0.1"
    port: 1080

自用配置

# Optimized DNS Server Configuration  
# Based on best practices from sing-box, Xray-core, mihomo  
  
# Server Configuration  
server:  
  port: 10053  
  protocol: udp  # or: tcp, both  
  bind: 0.0.0.0  # Listen address  
  
# Bootstrap DNS for resolving nameserver hostnames  
bootstrap:  
  nameservers:  
    - 223.5.5.5  
    - 119.29.29.29  
  
# Upstream DNS Groups  
upstream_group:  
  # Foreign DNS through proxy (no ECS for privacy)  
  proxy:  
    nameservers:  
      - https://1.1.1.1/dns-query  
      - https://8.8.8.8/dns-query  
    outbound: hk  
  
  # Foreign DNS through proxy with ECS (for CDN optimization)  
  proxy_ecs:  
    nameservers:  
      - https://8.8.8.8/dns-query  
      - https://8.8.4.4/dns-query  
    outbound: hk  
  
  # Foreign DNS through proxy with ECS (for CDN optimization)  
  proxy_us:  
    nameservers:  
      - https://1.1.1.1/dns-query  
      - https://8.8.8.8/dns-query  
    outbound: us  
  
  
  # Domestic DNS (direct connection)  
  direct:  
    nameservers:  
      - 240e:f:a::6  
      - 61.134.1.4  
      - 218.30.19.40  
      - 240e:f:a00b::6  
      - 223.5.5.5  
      - 119.29.29.29  
    outbound: direct  
  
# Outbound (SOCKS5 Proxies)  
outbound:  
  - tag: direct  
    type: direct  # Built-in, no proxy  
  
  - tag: file_download  
    type: socks5  
    enable: true  
    server: 10.115.15.1  
    port: 17890  
    username: hk  
    password: hk  
  
  - tag: hk  
    type: socks5  
    enable: true  
    server: 10.115.15.1  
    port: 17890  
    username: hk  
    password: hk  
  
  - tag: us  
    type: socks5  
    enable: true  
    server: 10.115.15.1  
    port: 17891  
    username: us  
    password: us  
  
# ECS (EDNS Client Subnet) Global Configuration  
ecs:  
  enable: true  
  default_ipv4: 113.132.219.0/24  
  default_ipv6: 240e:358:a07:94c2:d589:ebf2:9d2:be0e/64  
  ipv4_prefix: 24  # Send /24 prefix (hides last octet for privacy)  
  ipv6_prefix: 64  # Send /64 prefix  
  
# Cache Configuration  
cache:  
  dns_cache:  
    enable: true  
    clear: false  # IMPORTANT: Don't clear in production  
    type: redis  # or: memory  
  
  category_cache:  
    enable: true  
    clear: false  
    type: redis  
    ttl: 604800  # 7 days (categories rarely change)  
  
# Redis Configuration  
redis:  
  server: 10.115.15.25  
  port: 6379  
  database: 8  
  password: "violet-dns-secure-2024"  
  max_retries: 3  
  pool_size: 10  
  
# Domain Categorization Policy  
category_policy:  
  preload:  
    file: 'https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat'  
    domain_group:  
      ads_site:  
        - category-ads-all  
      ai_site:  
        - category-ai-chat-!cn  
      game_domain:  
        - category-games  
      proxy_site:  
        - google  
      direct_site:  
  
  
# Query Routing Policy (matched top-down)  
query_policy:  
  # Block ads and trackers  
  - name: ads_site  
    group: block  
    options:  
      block_type: nxdomain  # or: noerror, 0.0.0.0  
      block_ttl: 60  # Block record TTL in seconds  
  
  # Game domains (low latency needed)  - name: game_domain  
    group: direct  
    options:  
      strategy: prefer_ipv4  
  
  - name: ai_site  
    group: proxy_us  
    options:  
      strategy: prefer_ipv4  
      disable_https: true  
  
  # Foreign sites through proxy  
  - name: proxy_site  
    group: proxy  
    options:  
      strategy: prefer_ipv4  
      expected_ips:  # Only accept non-CN IPs  
        - geoip:!cn  
        - geoip:private  
      fallback_group: direct  
  
  # Domestic sites direct  
  - name: direct_site  
    group: direct  
    options:  
      strategy: prefer_ipv4  
      expected_ips:  
        - geoip:cn  
  
  # Unknown domains: intelligent fallback  
  - name: unknown  
    group: proxy_ecs_fallback  
    options:  
      strategy: prefer_ipv4  
      auto_categorize: true  # Update category_cache based on result  
  
# Fallback Configuration (for proxy_ecs_fallback policy)  
fallback:  
  geoip: 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/GeoLite2-Country.mmdb'  
  asn: 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/GeoLite2-ASN.mmdb'  
  update: '0 0 7 * * *'  # Update at 3 AM daily  
  strategy: race  # Race both proxy_ecs and proxy  
  rule:  
    - geoip:cn  
    - geoip:private  
    - asn:4134  # China Telecom  
    - asn:4837  # China Unicom  
    - asn:9808  # China Mobile  
  # If IP matches rule → use direct, else use proxy  
# Logging Configuration  
log:  
  level: info           # 日志级别: debug, info, warn, error  
  format: json           # 日志格式: json (结构化), text (带颜色的文本)  
  output: stdout         # 输出位置: stdout (控制台), file (默认violet-dns.log), 或自定义路径  
  max_size: 100          # 单个日志文件最大大小 (MB),默认 100MB  max_age: 7             # 日志文件保留天数,默认 7 天  
  max_backups: 10        # 保留的旧日志文件数量,默认 10 个  
  compress: true         # 是否压缩旧日志文件 (gzip)  total_size_limit: 500  # 所有日志文件总大小限制 (MB),超过后自动删除最旧的文件

构建

未提供产物,需要自行从源码构建

Linux

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build

OpenWRT

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath

部署

Systemd 服务

[Unit]
Description=violet-dns
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/violet-dns -d /etc/violet-dns
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

使用建议

预先导入category_policy中定义的网站到redis中 此处不需要定义cn和非cn网站,等待后续自动分配即可(因为此种分流必然是不完整且有错误的不如不导入) 只需要定义需要单独分流的域名即可

其余的配置看配文件足够简单明了

violet-dns -d /var/run/violet-dns -load

文章标题:一个自动分类的DNS服务器

文章作者:violet

文章链接:https://www.vio.vin/posts/yi-ge-zi-dong-fen-lei-de-dns-fu-wu-qi[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。