不能用net/http搭DNS转发代理,因DNS协议基于UDP/TCP而非HTTP;正确做法是用miekg/dns监听UDP/TCP端口,解析并转发DNS报文至上游服务器(如8.8.8.8),需处理RD标志、ID匹配、EDNS0兼容及权限问题。

用 net/http 搭一个能转发 DNS 查询的 HTTP 代理服务?不行,得换路子
Go 本身不带 DNS 代理能力,net/http 只处理 HTTP 协议,而域名解析走的是 DNS 协议(通常是 UDP 53 端口)。想实现“域名解析代理”,本质是写一个 DNS 转发器(DNS forwarder),不是 HTTP 代理。常见误区是试图用 http.ListenAndServe 接 DNS 查询,结果连包都收不到——DNS 查询压根不是 HTTP 请求。
正确路径是:监听 UDP(和可选 TCP)53 端口,解析收到的 DNS 查询报文,转发给上游 DNS(比如 8.8.8.8),再把响应原样或稍作修改后发回客户端。
- DNS 查询默认走 UDP,大于 512 字节或涉及某些记录类型(如
TXT、EDNS0)可能触发 TCP 回退 -
net.ListenUDP足够处理大多数场景;若需完整支持(如区域传输、大响应),才需补 TCP 监听 - 别自己手撕 DNS 报文二进制格式——用
github.com/miekg/dns,它封装了编码/解码、类型常量、常见错误处理
用 miekg/dns 实现最简 DNS 转发器,三步到位
核心逻辑就三件事:收包 → 改查询目标 → 发给上游 → 回传。下面是最小可行代码(省略错误处理细节,仅示意主干):
package main
<p>import (
"log"
"net"
"github.com/miekg/dns"
)</p><p>func main() {
udpAddr, <em> := net.ResolveUDPAddr("udp", ":53")
conn, </em> := net.ListenUDP("udp", udpAddr)
defer conn.Close()</p><pre class="brush:php;toolbar:false;">upstream := &dns.Client{Timeout: 5 * time.Second}
for {
buf := make([]byte, 512)
n, addr, _ := conn.ReadFromUDP(buf)
if n == 0 {
continue
}
m := new(dns.Msg)
m.Unpack(buf[:n])
// 构造新查询:复用 ID、Question,只改 RecursionDesired 和 NS
req := new(dns.Msg)
req.Id = m.Id
req.RecursionDesired = true
req.Question = m.Question
r, _, err := upstream.Exchange(req, "8.8.8.8:53")
if err != nil || r == nil {
continue
}
respBuf, _ := r.Pack()
conn.WriteToUDP(respBuf, addr)
}}
- 必须保留原始
m.Id,否则客户端无法匹配响应 -
RecursionDesired = true是关键,否则上游 DNS 可能返回 REFUSED 或空响应 - 不要直接转发原始
m,因为其中可能含 EDNS0 OPT 记录,而上游(如8.8.8.8)对 OPT 处理较敏感;新建Msg更稳妥 -
buf初始大小设为 512 是 DNS UDP 基线,实际建议用 2048 防止截断(尤其启用 EDNS0 后)
本地测试时 dig @127.0.0.1 example.com 没反应?检查这几点
常见卡点不在代码逻辑,而在环境和权限:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- macOS / Linux 下绑定
:53需 root 权限:sudo go run main.go;Windows 可能需管理员 CMD - macOS 自带 mDNSResponder 占用 53 端口,先停掉:
sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist -
dig默认发 UDP,但若服务只监听 UDP 而你测试用了+tcp,会超时;加+notcp强制 UDP 验证 - 防火墙拦截:Linux 检查
ufw,macOS 查“系统设置 > 网络 > 防火墙”是否放行 - 确认
dig @127.0.0.1 example.com中的@127.0.0.1显式指定了你的代理地址,而非系统默认 DNS
加个日志或简单过滤,别碰 dns.Msg 的底层字段
想记录查询域名或屏蔽某些域名?直接操作 m.Question 切片即可,别碰 m.Answer 或 m.Extra 的原始字节:
// 记录查询域名
for _, q := range m.Question {
log.Printf("Query: %s %s", q.Name, dns.TypeToString[q.Qtype])
}
<p>// 屏蔽 example.com(返回 NXDOMAIN)
if m.Question[0].Name == "example.com." {
r := new(dns.Msg)
r.SetRcode(m, dns.RcodeNameError)
r.Authoritative = true
// ... pack & send
}
-
q.Name末尾带点("example.com."),这是 DNS 标准 FQDN 格式,匹配时别漏掉 - 修改响应时优先用
dns.Msg方法(如SetRcode、Answer添加记录),避免手动构造RR字段出错 - 高并发下
conn.ReadFromUDP是瓶颈,后续要支持多连接得上golang.org/x/net/netutil.LimitListener或改用dns.Server结构体
真正麻烦的从来不是转发逻辑,而是 UDP 包丢失、EDNS0 兼容性、TCP 回退重试、以及上游 DNS 的各种非标行为——这些在玩具版里可以忽略,但上线前绕不开。

















