golang使用DoH解析域名

按照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)
		}
	}
}
相关推荐
hlsd#35 分钟前
go 集成go-redis 缓存操作
redis·缓存·golang
qq_1728055914 小时前
GIN 反向代理功能
后端·golang·go
__AtYou__15 小时前
Golang | Leetcode Golang题解之第535题TinyURL的加密与解密
leetcode·golang·题解
kevin_tech19 小时前
Go API 多种响应的规范化处理和简化策略
开发语言·后端·golang·状态模式
幺零九零零1 天前
【Golang】sql.Null* 类型使用(处理空值和零值)
数据库·sql·golang
cookies_s_s1 天前
Golang--DOS命令、变量、基本数据类型、标识符
golang
__AtYou__1 天前
Golang | Leetcode Golang题解之第541题反转字符串II
leetcode·golang·题解
flying robot1 天前
Go的JSON转化
golang
幺零九零零1 天前
【Golang】validator库的使用
开发语言·后端·golang
海绵宝宝de派小星1 天前
Go:接口和反射
开发语言·后端·golang