【golang】DNS 资源记录(RR)接口

发布于:2025-05-16 ⋅ 阅读:(11) ⋅ 点赞:(0)

Go 中 miekg/dns 包对 DNS 资源记录(RR)接口 的定义:

type RR interface {
	Header() *RR_Header
	String() string
	copy() RR
	len(off int, compression map[string]struct{}) int
	pack(...)
	unpack(...)
	parse(...)
	isDuplicate(r2 RR) bool
}

这个接口定义了一个资源记录(RR)应具备的核心功能。


每个方法的作用

方法名 作用
Header() 返回资源记录的头部(RR_Header),包含名称、TTL、类型等元数据。
String() 以字符串格式返回记录,常用于调试或输出为 zone 文件格式。
copy() 返回该 RR 的深拷贝。
len(...) 返回该记录的字节长度(压缩或未压缩形式)。
pack(...) 把记录转换为 DNS 报文(二进制 wire 格式)的一部分。
unpack(...) 从 wire 格式解析出该记录内容。
parse(...) 从 zone 文件解析一个 RR。
isDuplicate(...) 判断两个 RR 是否是重复的。

✅ 实战演示:自定义并使用 RR 接口对象

我们用 dns.NewRR() 来创建一个 RR 实例,然后使用其中的一些方法。

package main

import (
	"fmt"
	"github.com/miekg/dns"
)

func main() {
	// 创建一个 RR(类型是 A)
	rr, err := dns.NewRR("example.com. 3600 IN A 93.184.216.34")
	if err != nil {
		panic(err)
	}

	// 查看 RR 的字符串表示
	fmt.Println("RR String():", rr.String())

	// 获取 RR 的头部
	header := rr.Header()
	fmt.Printf("Name: %s\nType: %d\nClass: %d\nTTL: %d\n",
		header.Name, header.Rrtype, header.Class, header.Ttl)

	// 拷贝这个 RR
	rrCopy := rr.Copy()
	fmt.Println("Copy equals original?", rrCopy.String() == rr.String())
}

输出示例:

RR String(): example.com.	3600	IN	A	93.184.216.34
Name: example.com.
Type: 1
Class: 1
TTL: 3600
Copy equals original? true

🔍 RR_Header 是什么?

资源记录都有一个头部,称为 RR_Header

type RR_Header struct {
	Name     string
	Rrtype   uint16
	Class    uint16
	Ttl      uint32
	Rdlength uint16 // 自动填充
}

这个头部定义了一个资源记录的“元信息”,类似于HTTP报文中的Header部分。


🚨 高阶用法:自定义打包/解析

方法如 pack, unpack, parse, len 等主要用于 DNS 底层协议处理(比如实现一个 DNS 服务器或手动构建报文)。通常你不需要自己实现这些接口,因为 miekg/dns 提供了所有常见类型的内置支持(如 A, MX, TXT, CNAME)。


如何自定义一个实现了 RR 接口的结构体

如何解析自定义类型记录

非常好,这是一个高级又非常实用的用法——自定义 DNS RR 类型并实现 dns.RR 接口

你可以通过实现 RR 接口中的所有方法,来定义自己的 DNS 资源记录类型(比如:一个私有记录 TYPE65534)。这在你实现某些自定义协议或扩展 DNS 时非常有用。


🛠 示例目标

我们自定义一个记录类型:XINFO(私有类型码:65534),记录中包含一个文本字符串。

Zone 格式示意:

custom.example.com. 3600 IN XINFO "my custom info"

✅ 第一步:定义结构体

package main

import (
	"fmt"
	"strings"

	"github.com/miekg/dns"
)

// 自定义 RR 类型 XINFO
type XINFO struct {
	Hdr  dns.RR_Header
	Info string
}

// 实现 dns.RR 接口

func (x *XINFO) Header() *dns.RR_Header { return &x.Hdr }

func (x *XINFO) String() string {
	return fmt.Sprintf("%s\t%d\t%s\tXINFO\t\"%s\"",
		x.Hdr.Name, x.Hdr.Ttl, dns.ClassToString[x.Hdr.Class], x.Info)
}

func (x *XINFO) copy() dns.RR {
	return &XINFO{
		Hdr:  x.Hdr,
		Info: x.Info,
	}
}

func (x *XINFO) len(off int, compression map[string]struct{}) int {
	// 2 bytes for TXT length prefix + content
	return x.Hdr.Len(off, compression) + 1 + len(x.Info)
}

func (x *XINFO) pack(msg []byte, off int, compression dns.CompressionMap, compress bool) (int, error) {
	off, err := x.Hdr.Pack(msg, off, compression, compress)
	if err != nil {
		return off, err
	}
	msg[off] = byte(len(x.Info)) // 单段 TXT 记录格式
	off++
	copy(msg[off:], x.Info)
	return off + len(x.Info), nil
}

func (x *XINFO) unpack(msg []byte, off int) (int, error) {
	length := int(msg[off])
	off++
	x.Info = string(msg[off : off+length])
	return off + length, nil
}

func (x *XINFO) parse(c *dns.Zlexer, origin string) *dns.ParseError {
	tok, _ := c.Next()
	if tok.Value == "" {
		return &dns.ParseError{Err: "no value for XINFO"}
	}
	x.Info = strings.Trim(tok.Value, `"`)
	return nil
}

func (x *XINFO) isDuplicate(r2 dns.RR) bool {
	xx, ok := r2.(*XINFO)
	if !ok {
		return false
	}
	return x.Info == xx.Info
}

✅ 第二步:注册自定义 RR 类型

func init() {
	const TypeXINFO = 65534
	dns.TypeToString[TypeXINFO] = "XINFO"
	dns.StringToType["XINFO"] = TypeXINFO

	// 注册解析器
	dns.TypeToRR[TypeXINFO] = func() dns.RR {
		return new(XINFO)
	}
}

✅ 第三步:解析记录字符串

func main() {
	rr, err := dns.NewRR("custom.example.com. 3600 IN XINFO \"my custom info\"")
	if err != nil {
		panic(err)
	}
	fmt.Println(rr.String())

	// 类型断言并访问 Info 字段
	if xinfo, ok := rr.(*XINFO); ok {
		fmt.Println("Info:", xinfo.Info)
	}
}

🧾 输出示例:

custom.example.com.	3600	IN	XINFO	"my custom info"
Info: my custom info

✅ 小结

实现自定义 RR 类型,你需要:

  1. 定义结构体,包含 dns.RR_Header 和你自定义的数据字段。
  2. 实现 dns.RR 接口 的所有方法。
  3. 注册你的类型码、名字与工厂函数(dns.TypeToRR[...]
  4. 使用 dns.NewRR(...)dns.UnpackRR(...) 正常解析。

如果你还想让这个类型支持 JSON、Base64 编码,或序列化成 wire 格式,也可以在基础之上继续拓展。

用这个自定义类型发送或接收真实 DNS 查询

当然可以!我们来展示如何通过 Go 的 miekg/dns 包,使用你自定义的 RR 类型进行 DNS 查询或响应。由于 DNS 查询通常是客户端发起请求、服务端响应,我们分别说明:


📡 一、作为服务端:响应带自定义记录的 DNS 查询

这是最常见的用法:你运行一个 DNS 服务,并在某个请求域名时,返回你定义的 XINFO 类型记录。

✅ 服务端示例

package main

import (
	"fmt"
	"log"
	"net"

	"github.com/miekg/dns"
)

// init: 注册自定义类型 XINFO(65534)
func init() {
	const TypeXINFO = 65534
	dns.TypeToString[TypeXINFO] = "XINFO"
	dns.StringToType["XINFO"] = TypeXINFO
	dns.TypeToRR[TypeXINFO] = func() dns.RR {
		return new(XINFO)
	}
}

// 定义自定义 RR
type XINFO struct {
	Hdr  dns.RR_Header
	Info string
}

func (x *XINFO) Header() *dns.RR_Header                 { return &x.Hdr }
func (x *XINFO) String() string                         { return fmt.Sprintf("%s\t%d\tIN\tXINFO\t\"%s\"", x.Hdr.Name, x.Hdr.Ttl, x.Info) }
func (x *XINFO) copy() dns.RR                           { cp := *x; return &cp }
func (x *XINFO) len(off int, c map[string]struct{}) int { return x.Hdr.Len(off, c) + 1 + len(x.Info) }
func (x *XINFO) pack(buf []byte, off int, c dns.CompressionMap, compress bool) (int, error) {
	off, err := x.Hdr.Pack(buf, off, c, compress)
	if err != nil {
		return off, err
	}
	buf[off] = byte(len(x.Info))
	copy(buf[off+1:], x.Info)
	return off + 1 + len(x.Info), nil
}
func (x *XINFO) unpack(msg []byte, off int) (int, error) {
	length := int(msg[off])
	off++
	x.Info = string(msg[off : off+length])
	return off + length, nil
}
func (x *XINFO) parse(c *dns.Zlexer, origin string) *dns.ParseError {
	tok, _ := c.Next()
	x.Info = tok.Value
	return nil
}
func (x *XINFO) isDuplicate(r2 dns.RR) bool {
	xx, ok := r2.(*XINFO)
	return ok && xx.Info == x.Info
}

// DNS 请求处理函数
func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
	msg := new(dns.Msg)
	msg.SetReply(r)

	for _, q := range r.Question {
		if q.Name == "custom.example.com." && q.Qtype == 65534 {
			rr := &XINFO{
				Hdr: dns.RR_Header{
					Name:   q.Name,
					Rrtype: 65534,
					Class:  dns.ClassINET,
					Ttl:    60,
				},
				Info: "Hello from custom XINFO!",
			}
			msg.Answer = append(msg.Answer, rr)
		}
	}

	w.WriteMsg(msg)
}

func main() {
	server := &dns.Server{Addr: ":5353", Net: "udp"}
	dns.HandleFunc(".", handleDNSRequest)

	fmt.Println("DNS server listening on UDP :5353")
	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

🧪 测试方法:模拟查询

你可以写个测试客户端,发送一个请求到这个服务:

package main

import (
	"fmt"
	"github.com/miekg/dns"
)

func main() {
	c := new(dns.Client)
	m := new(dns.Msg)
	m.SetQuestion("custom.example.com.", 65534)

	resp, _, err := c.Exchange(m, "127.0.0.1:5353")
	if err != nil {
		panic(err)
	}

	for _, ans := range resp.Answer {
		fmt.Println("Response:", ans.String())
	}
}

🧾 输出(示例):

DNS server listening on UDP :5353
Response: custom.example.com.	60	IN	XINFO	"Hello from custom XINFO!"

🔐 注意事项

  • 自定义类型不会被标准 DNS 服务器识别。类型号 65534 被保留为私有用途(RFC 6895)。
  • 如果你希望在生产中用自定义 RR,建议配合专用客户端或私有 DNS 协议。

✅ 总结

你现在知道了:

  1. 如何定义自定义的 DNS RR 类型(如 XINFO)。
  2. 如何注册并使用 miekg/dns 支持这个类型。
  3. 如何启动一个支持自定义记录的 DNS 服务端。
  4. 如何用 Go 客户端去解析自定义记录。

使用 dig 命令行工具测试

dig 命令行工具测试你自定义的 DNS 服务器和自定义的 RR 类型(比如我们示例中的 XINFO)。


1. 前提条件

  • 你的自定义 DNS 服务器已经运行在本地,比如监听在 UDP 端口 5353(代码示例中的端口)。
  • 服务器可以正确响应你的自定义 RR 查询(类型号 65534,名字是 XINFO)。

2. 使用 dig 查询自定义类型

dig 默认识别标准的DNS类型,如 A、MX、TXT 等。要查询一个自定义类型,需要使用类型号查询。

假设你的自定义类型码是 65534,查询命令如下:

dig @127.0.0.1 -p 5353 custom.example.com. TYPE65534 +noall +answer

解释:

  • @127.0.0.1:指定向本地服务器查询。
  • -p 5353:指定端口(你的服务器端口)。
  • custom.example.com.:查询的域名。
  • TYPE65534:查询类型为编号 65534(你的自定义类型)。
  • +noall +answer:只显示响应中的 answer 部分,简洁输出。

3. 输出示例

如果服务器返回正常,你会看到类似:

custom.example.com. 60 IN TYPE65534 "Hello from custom XINFO!"

这里:

  • TYPE65534 是 dig 无法识别的类型,所以用 TYPE 加类型号显示。
  • 后面跟着你自定义记录的字符串内容。

4. 如果想让 dig 显示类型名字而不是 TYPE65534?

默认情况下,dig 不认识私有类型的名字,所以显示类型号是正常的。

你可以通过修改 dig 的类型映射文件或者使用标准类型测试。一般情况下,直接用 TYPE<number> 查询自定义类型即可。


5. 额外提示

  • dig 支持 UDP 和 TCP,默认是 UDP。
  • 如果你的 DNS 服务器支持 TCP,可以用 +tcp 参数。
  • 你还可以用 +trace+dnssec 等参数调试 DNS。

6. 总结示范

dig @127.0.0.1 -p 5353 custom.example.com. TYPE65534 +noall +answer

运行这个命令,你就能测试自定义 DNS 服务器对自定义 RR 的响应。


网站公告

今日签到

点亮在社区的每一天
去签到