Go 中 miekg/dns
包對 DNS 資源記錄(RR)接口 的定義:
type RR interface {Header() *RR_HeaderString() stringcopy() RRlen(off int, compression map[string]struct{}) intpack(...)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 mainimport ("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)// 拷貝這個 RRrrCopy := 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 stringRrtype uint16Class uint16Ttl uint32Rdlength 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 mainimport ("fmt""strings""github.com/miekg/dns"
)// 自定義 RR 類型 XINFO
type XINFO struct {Hdr dns.RR_HeaderInfo 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 + contentreturn 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 = 65534dns.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 類型,你需要:
- 定義結構體,包含
dns.RR_Header
和你自定義的數據字段。 - 實現
dns.RR
接口 的所有方法。 - 注冊你的類型碼、名字與工廠函數(
dns.TypeToRR[...]
)。 - 使用
dns.NewRR(...)
或dns.UnpackRR(...)
正常解析。
如果你還想讓這個類型支持 JSON、Base64 編碼,或序列化成 wire 格式,也可以在基礎之上繼續拓展。
用這個自定義類型發送或接收真實 DNS 查詢
當然可以!我們來展示如何通過 Go 的 miekg/dns
包,使用你自定義的 RR 類型進行 DNS 查詢或響應。由于 DNS 查詢通常是客戶端發起請求、服務端響應,我們分別說明:
📡 一、作為服務端:響應帶自定義記錄的 DNS 查詢
這是最常見的用法:你運行一個 DNS 服務,并在某個請求域名時,返回你定義的 XINFO
類型記錄。
? 服務端示例
package mainimport ("fmt""log""net""github.com/miekg/dns"
)// init: 注冊自定義類型 XINFO(65534)
func init() {const TypeXINFO = 65534dns.TypeToString[TypeXINFO] = "XINFO"dns.StringToType["XINFO"] = TypeXINFOdns.TypeToRR[TypeXINFO] = func() dns.RR {return new(XINFO)}
}// 定義自定義 RR
type XINFO struct {Hdr dns.RR_HeaderInfo 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.Valuereturn 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 mainimport ("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 協議。
? 總結
你現在知道了:
- 如何定義自定義的 DNS RR 類型(如
XINFO
)。 - 如何注冊并使用
miekg/dns
支持這個類型。 - 如何啟動一個支持自定義記錄的 DNS 服務端。
- 如何用 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 的響應。