按照RFC 8484 规范,DoH服务器支持GET或POST两种方式。
当使用GET方法,唯一的变量"dns"被赋值为base64url编码的DNS请求内容。
These examples use a DoH service with a URI Template of
"https://dnsserver.example.net/dns-query{?dns}" to resolve IN A
records.
The requests are represented as bodies with media type "application/
dns-message".
The first example request uses GET to request "www.example.com".
:method = GET
:scheme = https
:authority = dnsserver.example.net
:path = /dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB
accept = application/dns-message
当使用POST方法,DNS查询消息被包含在HTTP 请求的body中,
header需要包含 Content-Type application/dns-message。
The same DNS query for "www.example.com", using the POST method would
be:
:method = POST
:scheme = https
:authority = dnsserver.example.net
:path = /dns-query
accept = application/dns-message
content-type = application/dns-message
content-length = 33
<33 bytes represented by the following hex encoding>
00 00 01 00 00 01 00 00 00 00 00 00 03 77 77 77
07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 00 01 00
01
DNS报文格式,具体请参看 RFC1035 ,本文不再赘述。
golang程序中dns报文封装推荐使用golang.org/x/net/dns/dnsmessage
下面先使用另一个库github.com/miekg/dns简要演示使用opendns DoH解析域名的GET和POST方式。
go
package main
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/miekg/dns"
"io"
"net/http"
)
// https://datatracker.ietf.org/doc/html/rfc8484#section-4.1.1
func main() {
rfc8484Get()
rfc8484Post()
}
func rfc8484Get() {
query := dns.Msg{}
query.SetQuestion("www.github.com.", dns.TypeA)
dsnReq, _ := query.Pack()
base64Query := base64.RawURLEncoding.EncodeToString(dsnReq)
client := &http.Client{}
//GET请求必须具有 ?dns=查询参数,该参数带有采用 Base64Url编码的DNS消息
req, err := http.NewRequest("GET", "https://doh.opendns.com/dns-query?dns="+base64Query, nil)
if err != nil {
fmt.Printf("Send query error, err:%v\n", err)
return
}
req.Header.Add("Accept", "application/dns-message")
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
response := dns.Msg{}
response.Unpack(bodyBytes)
fmt.Printf("Dns answer is :%v\n", response.String())
}
func rfc8484Post() {
query := dns.Msg{}
query.SetQuestion("www.github.com.", dns.TypeA)
dsnReq, _ := query.Pack()
client := &http.Client{}
//POST请求正文为二进制DNS消息
req, err := http.NewRequest("POST", "https://doh.opendns.com/dns-query", bytes.NewBuffer(dsnReq))
if err != nil {
fmt.Printf("Send query error, err:%v\n", err)
}
req.Header.Add("Accept", "application/dns-message")
//POST头需要包含 Content-Type application/dns-message
req.Header.Add("Content-Type", "application/dns-message")
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
response := dns.Msg{}
response.Unpack(bodyBytes)
fmt.Printf("Dns answer is :%v\n", response.String())
}
上面response.String()直观显示了返回数据,下面再使用golang.org/x/net/dns/dnsmessage演示读取返回具体数据格式
go
package main
import (
"bytes"
"fmt"
"golang.org/x/net/dns/dnsmessage"
"io"
"math/rand"
"net"
"net/http"
"os"
"strings"
)
func main() {
query := dnsmessage.Message{
Header: dnsmessage.Header{
ID: uint16(rand.Intn(65535) + 1), // Unique identifier for the query
Response: false, // This is a query, not a response
RecursionDesired: true, // Ask for recursive resolution
},
Questions: []dnsmessage.Question{
{
Name: dnsmessage.MustNewName("www.github.com."), // Domain name to query
Type: dnsmessage.TypeA, // Query type (A record)
Class: dnsmessage.ClassINET, // Internet class
},
},
}
queryBytes, err := query.Pack()
if err != nil {
fmt.Println("Failed to pack DNS query:", err)
return
}
client := &http.Client{}
//POST请求正文为二进制DNS消息
req, err := http.NewRequest("POST", "https://doh.opendns.com/dns-query", bytes.NewBuffer(queryBytes))
if err != nil {
fmt.Printf("Send query error, err:%v\n", err)
return
}
req.Header.Add("Accept", "application/dns-message")
//POST头需要包含 Content-Type application/dns-message
req.Header.Add("Content-Type", "application/dns-message")
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
var dnsResponse dnsmessage.Message
err = dnsResponse.Unpack(bodyBytes)
if err != nil {
fmt.Println("Failed to unpack DNS response:", err)
os.Exit(1)
}
// 遍历响应资源记录
for _, answer := range dnsResponse.Answers {
switch answer.Header.Type {
case dnsmessage.TypeA:
fmt.Println("Type A Record:")
fmt.Println("IP Address:", net.IP(answer.Body.(*dnsmessage.AResource).A[:]).String())
case dnsmessage.TypeNS:
fmt.Println("Type NS Record:")
fmt.Println("Name Server:", answer.Body.(*dnsmessage.NSResource).NS.String())
case dnsmessage.TypeCNAME:
fmt.Println("Type CNAME Record:")
fmt.Println("Canonical Name:", answer.Body.(*dnsmessage.CNAMEResource).CNAME.String())
case dnsmessage.TypeSOA:
fmt.Println("Type SOA Record:")
soa := answer.Body.(*dnsmessage.SOAResource)
fmt.Println("Primary Name Server:", soa.NS.String())
fmt.Println("Responsible Person:", soa.MBox.String())
fmt.Println("Serial Number:", soa.Serial)
fmt.Println("Refresh Interval:", soa.Refresh)
fmt.Println("Retry Interval:", soa.Retry)
fmt.Println("Expire Limit:", soa.Expire)
fmt.Println("Minimum TTL:", soa.MinTTL)
case dnsmessage.TypePTR:
fmt.Println("Type PTR Record:")
fmt.Println("Pointer Domain Name:", answer.Body.(*dnsmessage.PTRResource).PTR.String())
case dnsmessage.TypeMX:
fmt.Println("Type MX Record:")
mx := answer.Body.(*dnsmessage.MXResource)
fmt.Println("Mail Exchange:", mx.MX.String())
fmt.Println("Preference:", mx.Pref)
case dnsmessage.TypeTXT:
fmt.Println("Type TXT Record:")
fmt.Println("Text Data:", strings.Join(answer.Body.(*dnsmessage.TXTResource).TXT, ""))
case dnsmessage.TypeAAAA:
fmt.Println("Type AAAA Record:")
fmt.Println("IPv6 Address:", net.IP(answer.Body.(*dnsmessage.AAAAResource).AAAA[:]))
case dnsmessage.TypeSRV:
fmt.Println("Type SRV Record:")
srv := answer.Body.(*dnsmessage.SRVResource)
fmt.Println("Priority:", srv.Priority)
fmt.Println("Weight:", srv.Weight)
fmt.Println("Port:", srv.Port)
fmt.Println("Target:", srv.Target.String())
case dnsmessage.TypeOPT:
fmt.Println("Type OPT Record:")
// 一般情况下,OPT记录用于 DNS 扩展,不是常规资源记录。
default:
fmt.Println("Unhandled record type:", answer.Header.Type)
}
}
}